Skip to content

Commit

Permalink
Merge pull request #238 from mikomatic/issue-228
Browse files Browse the repository at this point in the history
Add the possibility to define a layer using a DescribedPredicate

Resolves: #228
  • Loading branch information
codecholeric authored Oct 20, 2019
2 parents 5f0d804 + 28b32a0 commit e1d5cc2
Show file tree
Hide file tree
Showing 2 changed files with 112 additions and 64 deletions.
119 changes: 77 additions & 42 deletions archunit/src/main/java/com/tngtech/archunit/library/Architectures.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,18 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableSet;
import com.tngtech.archunit.PublicAPI;
import com.tngtech.archunit.base.DescribedPredicate;
import com.tngtech.archunit.base.Optional;
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.JavaClasses;
Expand All @@ -44,19 +41,23 @@
import com.tngtech.archunit.lang.syntax.PredicateAggregator;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.base.Strings.isNullOrEmpty;
import static com.google.common.collect.Lists.newArrayList;
import static com.tngtech.archunit.PublicAPI.Usage.ACCESS;
import static com.tngtech.archunit.base.DescribedPredicate.alwaysFalse;
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.JavaClass.Predicates.equivalentTo;
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.resideInAnyPackage;
import static com.tngtech.archunit.core.domain.properties.HasName.Predicates.name;
import static com.tngtech.archunit.lang.SimpleConditionEvent.violated;
import static com.tngtech.archunit.lang.conditions.ArchConditions.onlyHaveDependentsWhere;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;
import static java.lang.System.lineSeparator;
import static java.util.Arrays.asList;
import static java.util.Collections.singleton;

/**
* Offers convenience to assert typical architectures, like a {@link #layeredArchitecture()}.
Expand Down Expand Up @@ -97,19 +98,19 @@ public static LayeredArchitecture layeredArchitecture() {
}

public static final class LayeredArchitecture implements ArchRule {
private final Map<String, LayerDefinition> layerDefinitions;
private final LayerDefinitions layerDefinitions;
private final Set<LayerDependencySpecification> dependencySpecifications;
private final PredicateAggregator<Dependency> irrelevantDependenciesPredicate;
private final Optional<String> overriddenDescription;

private LayeredArchitecture() {
this(new LinkedHashMap<String, LayerDefinition>(),
this(new LayerDefinitions(),
new LinkedHashSet<LayerDependencySpecification>(),
new PredicateAggregator<Dependency>().thatORs(),
Optional.<String>absent());
}

private LayeredArchitecture(Map<String, LayerDefinition> layerDefinitions,
private LayeredArchitecture(LayerDefinitions layerDefinitions,
Set<LayerDependencySpecification> dependencySpecifications,
PredicateAggregator<Dependency> irrelevantDependenciesPredicate,
Optional<String> overriddenDescription) {
Expand All @@ -120,7 +121,7 @@ private LayeredArchitecture(Map<String, LayerDefinition> layerDefinitions,
}

private LayeredArchitecture addLayerDefinition(LayerDefinition definition) {
layerDefinitions.put(definition.name, definition);
layerDefinitions.add(definition);
return this;
}

Expand All @@ -142,7 +143,7 @@ public String getDescription() {
}

List<String> lines = newArrayList("Layered architecture consisting of");
for (LayerDefinition definition : layerDefinitions.values()) {
for (LayerDefinition definition : layerDefinitions) {
lines.add(definition.toString());
}
for (LayerDependencySpecification specification : dependencySpecifications) {
Expand All @@ -151,11 +152,16 @@ public String getDescription() {
return Joiner.on(lineSeparator()).join(lines);
}

@Override
public String toString() {
return getDescription();
}

@Override
@PublicAPI(usage = ACCESS)
public EvaluationResult evaluate(JavaClasses classes) {
EvaluationResult result = new EvaluationResult(this, Priority.MEDIUM);
for (LayerDefinition layerDefinition : layerDefinitions.values()) {
for (LayerDefinition layerDefinition : layerDefinitions) {
result.add(evaluateLayersShouldNotBeEmpty(classes, layerDefinition));
}
for (LayerDependencySpecification specification : dependencySpecifications) {
Expand All @@ -165,24 +171,21 @@ public EvaluationResult evaluate(JavaClasses classes) {
}

private EvaluationResult evaluateLayersShouldNotBeEmpty(JavaClasses classes, LayerDefinition layerDefinition) {
return classes().that().resideInAnyPackage(toArray(layerDefinition.packageIdentifiers))
return classes().that(layerDefinitions.containsPredicateFor(layerDefinition.name))
.should(notBeEmptyFor(layerDefinition))
.evaluate(classes);
}

private EvaluationResult evaluateDependenciesShouldBeSatisfied(JavaClasses classes, LayerDependencySpecification specification) {
SortedSet<String> packagesOfOwnLayer = packagesOf(specification.layerName);
SortedSet<String> packagesOfAllowedAccessors = packagesOf(specification.allowedAccessors);
packagesOfAllowedAccessors.addAll(packagesOfOwnLayer);

return classes().that().resideInAnyPackage(toArray(packagesOfOwnLayer))
.should(onlyHaveDependentsWhere(originPackageMatchesIfDependencyIsRelevant(packagesOfAllowedAccessors)))
return classes().that(layerDefinitions.containsPredicateFor(specification.layerName))
.should(onlyHaveDependentsWhere(originMatchesIfDependencyIsRelevant(specification.layerName, specification.allowedAccessors)))
.evaluate(classes);
}

private DescribedPredicate<Dependency> originPackageMatchesIfDependencyIsRelevant(SortedSet<String> packagesOfAllowedAccessors) {
private DescribedPredicate<Dependency> originMatchesIfDependencyIsRelevant(String ownLayer, Set<String> allowedAccessors) {
DescribedPredicate<Dependency> originPackageMatches =
dependencyOrigin(JavaClass.Functions.GET_PACKAGE_NAME.is(PackageMatchers.of(toArray(packagesOfAllowedAccessors))));
dependencyOrigin(layerDefinitions.containsPredicateFor(allowedAccessors)).or(dependencyOrigin(layerDefinitions.containsPredicateFor(ownLayer)));

return irrelevantDependenciesPredicate.isPresent() ?
originPackageMatches.or(irrelevantDependenciesPredicate.get()) :
Expand Down Expand Up @@ -253,52 +256,84 @@ public LayeredArchitecture ignoreDependency(
irrelevantDependenciesPredicate.add(dependency(origin, target)), overriddenDescription);
}

private String[] toArray(Set<String> strings) {
return strings.toArray(new String[0]);
@PublicAPI(usage = ACCESS)
public LayerDependencySpecification whereLayer(String name) {
checkLayerNamesExist(name);
return new LayerDependencySpecification(name);
}

private SortedSet<String> packagesOf(String layerName) {
return packagesOf(Collections.singleton(layerName));
private void checkLayerNamesExist(String... layerNames) {
for (String layerName : layerNames) {
checkArgument(layerDefinitions.containLayer(layerName), "There is no layer named '%s'", layerName);
}
}

private SortedSet<String> packagesOf(Set<String> allowedAccessorLayerNames) {
SortedSet<String> packageIdentifiers = new TreeSet<>();
for (String accessor : allowedAccessorLayerNames) {
packageIdentifiers.addAll(layerDefinitions.get(accessor).packageIdentifiers);
private static final class LayerDefinitions implements Iterable<LayerDefinition> {
private final Map<String, LayerDefinition> layerDefinitions = new LinkedHashMap<>();

void add(LayerDefinition definition) {
layerDefinitions.put(definition.name, definition);
}
return packageIdentifiers;
}

@PublicAPI(usage = ACCESS)
public LayerDependencySpecification whereLayer(String name) {
checkLayersExist(name);
return new LayerDependencySpecification(name);
}
boolean containLayer(String layerName) {
return layerDefinitions.containsKey(layerName);
}

private void checkLayersExist(String... layerNames) {
for (String layerName : layerNames) {
checkArgument(layerDefinitions.containsKey(layerName), "There is no layer named '%s'", layerName);
DescribedPredicate<JavaClass> containsPredicateFor(String layerName) {
return containsPredicateFor(singleton(layerName));
}

DescribedPredicate<JavaClass> containsPredicateFor(final Collection<String> layerNames) {
DescribedPredicate<JavaClass> result = alwaysFalse();
for (LayerDefinition definition : get(layerNames)) {
result = result.or(definition.containsPredicate());
}
return result;
}

private Iterable<LayerDefinition> get(Collection<String> layerNames) {
Set<LayerDefinition> result = new HashSet<>();
for (String layerName : layerNames) {
result.add(layerDefinitions.get(layerName));
}
return result;
}

@Override
public Iterator<LayerDefinition> iterator() {
return layerDefinitions.values().iterator();
}
}

public final class LayerDefinition {
private final String name;
private Set<String> packageIdentifiers;
private DescribedPredicate<JavaClass> containsPredicate;

private LayerDefinition(String name) {
checkState(!isNullOrEmpty(name), "Layer name must be present");
this.name = name;
}

@PublicAPI(usage = ACCESS)
public LayeredArchitecture definedBy(String... packageIdentifiers) {
this.packageIdentifiers = ImmutableSet.copyOf(packageIdentifiers);
public LayeredArchitecture definedBy(DescribedPredicate<JavaClass> predicate) {
checkNotNull(predicate, "Supplied predicate must not be null");
this.containsPredicate = predicate;
return LayeredArchitecture.this.addLayerDefinition(this);
}

@PublicAPI(usage = ACCESS)
public LayeredArchitecture definedBy(String... packageIdentifiers) {
String description = String.format("'%s'", Joiner.on("', '").join(packageIdentifiers));
return definedBy(resideInAnyPackage(packageIdentifiers).as(description));
}

DescribedPredicate<JavaClass> containsPredicate() {
return containsPredicate;
}

@Override
public String toString() {
return String.format("layer '%s' ('%s')", name, Joiner.on("', '").join(packageIdentifiers));
return String.format("layer '%s' (%s)", name, containsPredicate);
}
}

Expand All @@ -319,7 +354,7 @@ public LayeredArchitecture mayNotBeAccessedByAnyLayer() {

@PublicAPI(usage = ACCESS)
public LayeredArchitecture mayOnlyBeAccessedByLayers(String... layerNames) {
checkLayersExist(layerNames);
checkLayerNamesExist(layerNames);
allowedAccessors.addAll(asList(layerNames));
descriptionSuffix = String.format("may only be accessed by layers ['%s']",
Joiner.on("', '").join(allowedAccessors));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,11 @@
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;

import static com.tngtech.archunit.core.domain.JavaClass.Predicates.resideInAnyPackage;
import static com.tngtech.archunit.core.domain.properties.HasName.Predicates.name;
import static com.tngtech.archunit.library.Architectures.layeredArchitecture;
import static com.tngtech.archunit.library.Architectures.onionArchitecture;
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.regex.Pattern.quote;
Expand All @@ -50,21 +52,39 @@ public class ArchitecturesTest {
@Rule
public final ExpectedException thrown = ExpectedException.none();

@Test
public void layered_architecture_description() {
LayeredArchitecture architecture = layeredArchitecture()
.layer("One").definedBy("some.pkg..")
.layer("Two").definedBy("first.any.pkg..", "second.any.pkg..")
.layer("Three").definedBy("..three..")
.whereLayer("One").mayNotBeAccessedByAnyLayer()
.whereLayer("Two").mayOnlyBeAccessedByLayers("One")
.whereLayer("Three").mayOnlyBeAccessedByLayers("One", "Two");
@DataProvider
public static Object[][] layeredArchitectureDefinitions() {
return testForEach(
layeredArchitecture()
.layer("One").definedBy("..library.testclasses.some.pkg..")
.layer("Two").definedBy("..library.testclasses.first.any.pkg..", "..library.testclasses.second.any.pkg..")
.layer("Three").definedBy("..library.testclasses..three..")
.whereLayer("One").mayNotBeAccessedByAnyLayer()
.whereLayer("Two").mayOnlyBeAccessedByLayers("One")
.whereLayer("Three").mayOnlyBeAccessedByLayers("One", "Two"),
layeredArchitecture()
.layer("One").definedBy(
resideInAnyPackage("..library.testclasses.some.pkg..")
.as("'..library.testclasses.some.pkg..'"))
.layer("Two").definedBy(
resideInAnyPackage("..library.testclasses.first.any.pkg..", "..library.testclasses.second.any.pkg..")
.as("'..library.testclasses.first.any.pkg..', '..library.testclasses.second.any.pkg..'"))
.layer("Three").definedBy(
resideInAnyPackage("..library.testclasses..three..")
.as("'..library.testclasses..three..'"))
.whereLayer("One").mayNotBeAccessedByAnyLayer()
.whereLayer("Two").mayOnlyBeAccessedByLayers("One")
.whereLayer("Three").mayOnlyBeAccessedByLayers("One", "Two"));
}

@Test
@UseDataProvider("layeredArchitectureDefinitions")
public void layered_architecture_description(LayeredArchitecture architecture) {
assertThat(architecture.getDescription()).isEqualTo(
"Layered architecture consisting of" + lineSeparator() +
"layer 'One' ('some.pkg..')" + lineSeparator() +
"layer 'Two' ('first.any.pkg..', 'second.any.pkg..')" + lineSeparator() +
"layer 'Three' ('..three..')" + lineSeparator() +
"layer 'One' ('..library.testclasses.some.pkg..')" + lineSeparator() +
"layer 'Two' ('..library.testclasses.first.any.pkg..', '..library.testclasses.second.any.pkg..')" + lineSeparator() +
"layer 'Three' ('..library.testclasses..three..')" + lineSeparator() +
"where layer 'One' may not be accessed by any layer" + lineSeparator() +
"where layer 'Two' may only be accessed by layers ['One']" + lineSeparator() +
"where layer 'Three' may only be accessed by layers ['One', 'Two']");
Expand Down Expand Up @@ -123,21 +143,14 @@ public void layered_architecture_defining_empty_layers_is_rejected() {
JavaClasses classes = new ClassFileImporter().importPackages(getClass().getPackage().getName() + ".testclasses");

EvaluationResult result = architecture.evaluate(classes);
assertThat(result.hasViolation()).isTrue();
assertThat(result.hasViolation()).as("result of evaluating empty layers has violation").isTrue();
assertPatternMatches(result.getFailureReport().getDetails(),
ImmutableSet.of(expectedEmptyLayer("Some"), expectedEmptyLayer("Other")));
}

@Test
public void layered_architecture_gathers_all_layer_violations() {
LayeredArchitecture architecture = layeredArchitecture()
.layer("One").definedBy(absolute("some.pkg.."))
.layer("Two").definedBy(absolute("first.any.pkg..", "second.any.pkg.."))
.layer("Three").definedBy(absolute("..three.."))
.whereLayer("One").mayNotBeAccessedByAnyLayer()
.whereLayer("Two").mayOnlyBeAccessedByLayers("One")
.whereLayer("Three").mayOnlyBeAccessedByLayers("One", "Two");

@UseDataProvider("layeredArchitectureDefinitions")
public void layered_architecture_gathers_all_layer_violations(LayeredArchitecture architecture) {
JavaClasses classes = new ClassFileImporter().importPackages(getClass().getPackage().getName() + ".testclasses");

EvaluationResult result = architecture.evaluate(classes);
Expand Down

0 comments on commit e1d5cc2

Please sign in to comment.