Skip to content

Commit

Permalink
add API to create ArchConditions from DescribedPredicates
Browse files Browse the repository at this point in the history
So far there has not been any straight forward way to turn a `DescribedPredicate` into an `ArchCondition`, even though the semantics are fairly similar (both determine if elements match a certain condition). The reason is, that `ArchCondition` needs more information than `DescribedPredicate`. While `DescribedPredicate` only needs to have some description to be displayed as part of the `ArchRule` text, `ArchCondition` additionally needs to describe each single event/violation that occurs. And the way how to do this depends on the predicate (take e.g. "does not *have* simple name 'Demo'" vs "is no enum" or "does not reside in a package 'com.demo'"). Thus, we have created a generic factory method `ArchCondition.from(predicate)` and two more convenient "sentence-like" factory methods `ArchConditions.{have/be}(predicate)` that can be used for most common cases. In any case if the event description does not fit it is possible to adjust it via `ConditionByPredicate.describeEventsBy(..)`.

Signed-off-by: Ulf Dreyer <ulf.dreyer@gmx.de>
Signed-off-by: Peter Gafert <peter.gafert@tngtech.com>
  • Loading branch information
u3r authored and codecholeric committed Jul 10, 2022
1 parent d5028fc commit 19839a1
Show file tree
Hide file tree
Showing 10 changed files with 371 additions and 457 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,20 @@ private Creator(String className, String simpleName) {
}

public ExpectedMessage notStartingWith(String prefix) {
return expectedSimpleName(String.format("does not start with '%s'", prefix));
return expectedClassViolation(String.format("does not have simple name starting with '%s'", prefix));
}

public ExpectedMessage notEndingWith(String suffix) {
return expectedSimpleName(String.format("does not end with '%s'", suffix));
return expectedClassViolation(String.format("does not have simple name ending with '%s'", suffix));
}

public ExpectedMessage containing(String infix) {
return expectedSimpleName(String.format("contains '%s'", infix));
return expectedClassViolation(String.format("has simple name containing '%s'", infix));
}

private ExpectedMessage expectedSimpleName(String suffix) {
return new ExpectedMessage(String.format("simple name of %s %s in (%s.java:0)",
className, suffix, simpleName));
private ExpectedMessage expectedClassViolation(String description) {
return new ExpectedMessage(String.format("Class <%s> %s in (%s.java:0)",
className, description, simpleName));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -137,12 +137,12 @@ private ClassAssertionCreator(Class<?> clazz) {
}

public MessageAssertionChain.Link havingNameMatching(String regex) {
return MessageAssertionChain.containsLine("Class <%s> matches '%s' in (%s.java:0)",
return MessageAssertionChain.containsLine("Class <%s> has name matching '%s' in (%s.java:0)",
clazz.getName(), regex, clazz.getSimpleName());
}

public MessageAssertionChain.Link havingSimpleNameContaining(String infix) {
return MessageAssertionChain.containsLine("simple name of %s contains '%s' in (%s.java:0)",
return MessageAssertionChain.containsLine("Class <%s> has simple name containing '%s' in (%s.java:0)",
clazz.getName(), infix, clazz.getSimpleName());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public <F> DescribedPredicate<F> onResultOf(final Function<? super F, ? extends
* Convenience method to downcast the predicate. {@link DescribedPredicate DescribedPredicates} are contravariant by nature,
* i.e. an {@code DescribedPredicate<T>} is an instance of {@code DescribedPredicate<V>}, if and only if {@code V} is an instance of {@code T}.
* <br>
* Take for example {@code Object > String}. Obviously an {@code DescribedPredicate<Object>} is also a {@code DescribedPredicate<String>}.
* Take for example {@code Object > String}. Obviously a {@code DescribedPredicate<Object>} is also a {@code DescribedPredicate<String>}.
* <br>
* Unfortunately, the Java type system does not allow us to express this property of the type parameter of {@code DescribedPredicate}.
* So to avoid forcing users to cast everywhere it is possible to use this method which also documents the intention and reasoning.
Expand Down
111 changes: 111 additions & 0 deletions archunit/src/main/java/com/tngtech/archunit/lang/ArchCondition.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,15 @@
import java.util.Collection;

import com.tngtech.archunit.PublicAPI;
import com.tngtech.archunit.base.DescribedPredicate;
import com.tngtech.archunit.base.HasDescription;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.properties.HasSourceCodeLocation;
import com.tngtech.archunit.lang.conditions.ArchConditions;

import static com.tngtech.archunit.PublicAPI.Usage.ACCESS;
import static com.tngtech.archunit.PublicAPI.Usage.INHERITANCE;
import static com.tngtech.archunit.lang.ConditionEvent.createMessage;

@PublicAPI(usage = INHERITANCE)
public abstract class ArchCondition<T> {
Expand Down Expand Up @@ -116,4 +122,109 @@ public String toString() {
public <U extends T> ArchCondition<U> forSubtype() {
return (ArchCondition<U>) this;
}

/**
* Creates an {@link ArchCondition} from a {@link DescribedPredicate}.
* For more information see {@link ConditionByPredicate ConditionByPredicate}.
* For more convenient versions of this method compare {@link ArchConditions#have(DescribedPredicate)} and {@link ArchConditions#be(DescribedPredicate)}.
*
* @param predicate Specifies which objects satisfy the condition.
* @return A {@link ConditionByPredicate ConditionByPredicate} derived from the supplied {@link DescribedPredicate predicate}
* @param <T> The type of object the {@link ArchCondition condition} will check
*
* @see ArchConditions#have(DescribedPredicate)
* @see ArchConditions#be(DescribedPredicate)
*/
@PublicAPI(usage = ACCESS)
public static <T extends HasDescription & HasSourceCodeLocation> ConditionByPredicate<T> from(DescribedPredicate<? super T> predicate) {
return new ConditionByPredicate<>(predicate);
}

/**
* An {@link ArchCondition} that derives which objects satisfy/violate the condition from a {@link DescribedPredicate}.
* The description is taken from the defining {@link DescribedPredicate predicate} but can be overridden via {@link #as(String, Object...)}.
* How the message of each single {@link ConditionEvent event} is derived can be customized by {@link #describeEventsBy(EventDescriber)}.
*
* @param <T> The type of object the condition will test
*/
@PublicAPI(usage = ACCESS)
public static final class ConditionByPredicate<T extends HasDescription & HasSourceCodeLocation> extends ArchCondition<T> {
private final DescribedPredicate<T> predicate;
private final EventDescriber eventDescriber;

private ConditionByPredicate(DescribedPredicate<? super T> predicate) {
this(predicate, predicate.getDescription(), ((predicateDescription, satisfied) -> (satisfied ? "satisfies " : "does not satisfy ") + predicateDescription));
}

private ConditionByPredicate(
DescribedPredicate<? super T> predicate,
String description,
EventDescriber eventDescriber
) {
super(description);
this.predicate = predicate.forSubtype();
this.eventDescriber = eventDescriber;
}

/**
* Adjusts how this {@link ConditionByPredicate condition} will create the description of the {@link ConditionEvent events}.
* E.g. assume the {@link DescribedPredicate predicate} of this condition is {@link JavaClass.Predicates#simpleName(String) simpleName(name)},
* then this method could be used to adjust the event description as
*
* <pre><code>
* condition.describeEventsBy((predicateDescription, satisfied) ->
* (satisfied ? "has " : "does not have ") + predicateDescription
* )</code></pre>
*
* @param eventDescriber Specifies how to create the description of the {@link ConditionEvent}
* whenever the predicate is evaluated against an object.
* @return A {@link ConditionByPredicate ConditionByPredicate} that describes its {@link ConditionEvent events} with the given {@link EventDescriber EventDescriber}
*/
@PublicAPI(usage = ACCESS)
public ConditionByPredicate<T> describeEventsBy(EventDescriber eventDescriber) {
return new ConditionByPredicate<>(
predicate,
getDescription(),
eventDescriber
);
}

@Override
public ConditionByPredicate<T> as(String description, Object... args) {
return new ConditionByPredicate<>(predicate, String.format(description, args), eventDescriber);
}

@Override
@SuppressWarnings("unchecked") // Cast is safe since input parameter is contravariant
public <U extends T> ConditionByPredicate<U> forSubtype() {
return (ConditionByPredicate<U>) this;
}

@Override
public void check(T object, ConditionEvents events) {
boolean satisfied = predicate.test(object);
String message = createMessage(object, eventDescriber.describe(predicate.getDescription(), satisfied));
events.add(new SimpleConditionEvent(object, satisfied, message));
}

/**
* Defines how to describe a single {@link ConditionEvent}. E.g. how to describe the concrete violation of some class
* {@code com.Example} that violates the {@link ConditionByPredicate}.
*/
@FunctionalInterface
@PublicAPI(usage = INHERITANCE)
public interface EventDescriber {
/**
* Describes a {@link ConditionEvent} created by {@link ConditionByPredicate ConditionByPredicate},
* given the description of the defining predicate and whether the predicate was satisfied.<br>
* For example, if the defining {@link DescribedPredicate} would be {@link JavaClass.Predicates#simpleName(String)}, then
* the created description could be {@code (satisfied ? "has " : "does not have ") + predicateDescription}.
*
* @param predicateDescription The description of the {@link DescribedPredicate} defining the {@link ConditionByPredicate ConditionByPredicate}
* @param satisfied Whether the object tested by the {@link ConditionByPredicate ConditionByPredicate} satisfied the condition
* @return The description of the {@link ConditionEvent} to be created
*/
String describe(String predicateDescription, boolean satisfied);
}
}
}
Loading

0 comments on commit 19839a1

Please sign in to comment.