Skip to content

Commit

Permalink
Add API to create ArchConditions from predicates TNG#904
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(..)`.
  • Loading branch information
codecholeric authored Jul 10, 2022
2 parents 2e3994d + 19839a1 commit 49f0722
Show file tree
Hide file tree
Showing 10 changed files with 420 additions and 459 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 @@ -51,6 +51,19 @@ public String getDescription() {
return description;
}

/**
* Overwrites the description of this {@link DescribedPredicate}. E.g.
*
* <pre><code>
* classes().that(predicate.as("some customized description with '%s'", "parameter")).should().bePublic()
* </code></pre>
*
* would then yield {@code classes that some customized description with 'parameter' should be public}.
*
* @param description The new description of this {@link DescribedPredicate}
* @param params Optional arguments to fill into the description via {@link String#format(String, Object...)}
* @return An {@link DescribedPredicate} with adjusted {@link #getDescription() description}.
*/
public DescribedPredicate<T> as(String description, Object... params) {
return new AsPredicate<>(this, description, params);
}
Expand All @@ -68,7 +81,16 @@ public <F> DescribedPredicate<F> onResultOf(final Function<? super F, ? extends
}

/**
* Workaround for the limitations of the Java type system {@code ->} Can't specify this contravariant type at the language level
* 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 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.
*
* @return A {@link DescribedPredicate} accepting a subtype of the predicate's actual type parameter {@code T}
* @param <U> A subtype of the {@link DescribedPredicate DescribedPredicate's} type parameter {@code T}
*/
@SuppressWarnings("unchecked") // DescribedPredicate is contravariant
public final <U extends T> DescribedPredicate<U> forSubtype() {
Expand Down
136 changes: 136 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 @@ -63,6 +69,19 @@ public String getDescription() {
return description;
}

/**
* Overwrites the description of this {@link ArchCondition}. E.g.
*
* <pre><code>
* classes().should(condition.as("some customized description with '%s'", "parameter"))
* </code></pre>
*
* would then yield {@code classes should some customized description with 'parameter'}.
*
* @param description The new description of this {@link ArchCondition}
* @param args Optional arguments to fill into the description via {@link String#format(String, Object...)}
* @return An {@link ArchCondition} with adjusted {@link #getDescription() description}.
*/
public ArchCondition<T> as(String description, Object... args) {
return new ArchCondition<T>(description, args) {
@Override
Expand All @@ -87,8 +106,125 @@ public String toString() {
return getDescription();
}

/**
* Convenience method to downcast the condition. {@link ArchCondition ArchConditions} are contravariant by nature,
* i.e. an {@code ArchCondition<T>} is an instance of {@code ArchCondition<V>}, if and only if {@code V} is an instance of {@code T}.
* <br>
* Take for example {@code Object > String}. Obviously an {@code ArchCondition<Object>} is also an {@code ArchCondition<String>}.
* <br>
* Unfortunately, the Java type system does not allow us to express this property of the type parameter of {@code ArchCondition}.
* So to avoid forcing users to cast everywhere it is possible to use this method which also documents the intention and reasoning.
*
* @return An {@link ArchCondition} accepting a subtype of the condition's actual type parameter {@code T}
* @param <U> A subtype of the {@link ArchCondition ArchCondition's} type parameter {@code T}
*/
@SuppressWarnings("unchecked") // Cast is safe since input parameter is contravariant
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 49f0722

Please sign in to comment.