Skip to content

Commit

Permalink
Qute: add logival AND and OR operators
Browse files Browse the repository at this point in the history
- relates to quarkusio#6369
  • Loading branch information
mkouba committed Sep 18, 2020
1 parent ebfeffc commit 268d8bb
Show file tree
Hide file tree
Showing 8 changed files with 119 additions and 28 deletions.
8 changes: 8 additions & 0 deletions docs/src/main/asciidoc/qute-reference.adoc
Expand Up @@ -334,6 +334,14 @@ This could be useful to access data for which the key is overridden:
|Ternary
|Shorthand for if-then-else statement. Unlike in <<if_section>> nested operators are not supported.
|`{item.isActive ? item.name : 'Inactive item'}` outputs the value of `item.name` if `item.isActive` resolves to `true`.

|Logical AND
|Outputs `true` if both parts are not `falsy` as described in the <<if_section>>. The parameter is only evaluated if needed.
|`{person.isActive && person.hasStyle}`

|Logical OR
|Outputs `true` if any of the parts is not `falsy` as described in the <<if_section>>. The parameter is only evaluated if needed.
|`{person.isActive || person.hasStyle}`
|===

TIP: The condition in a ternary operator evaluates to `true` if the value is not considered `falsy` as described in the <<if_section>>.
Expand Down
Expand Up @@ -12,6 +12,7 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
Expand Down Expand Up @@ -93,7 +94,7 @@
import io.quarkus.qute.Variant;
import io.quarkus.qute.api.ResourcePath;
import io.quarkus.qute.deployment.TemplatesAnalysisBuildItem.TemplateAnalysis;
import io.quarkus.qute.deployment.TypeCheckExcludeBuildItem.Check;
import io.quarkus.qute.deployment.TypeCheckExcludeBuildItem.TypeCheck;
import io.quarkus.qute.deployment.TypeInfos.Info;
import io.quarkus.qute.generator.ExtensionMethodGenerator;
import io.quarkus.qute.generator.ExtensionMethodGenerator.NamespaceResolverCreator;
Expand Down Expand Up @@ -493,7 +494,7 @@ static void validateNestedExpressions(ClassInfo rootClazz, Map<String, Match> re

if (member == null) {
// Test whether the validation should be skipped
Check check = new Check(info.isProperty() ? info.asProperty().name : info.asVirtualMethod().name,
TypeCheck check = new TypeCheck(info.isProperty() ? info.asProperty().name : info.asVirtualMethod().name,
match.clazz, info.part.isVirtualMethod() ? info.part.asVirtualMethod().getParameters().size() : -1);
if (isExcluded(check, excludes)) {
LOGGER.debugf(
Expand Down Expand Up @@ -914,16 +915,18 @@ TemplateVariantsBuildItem collectTemplateVariants(List<TemplatePathBuildItem> te
@BuildStep
void excludeTypeChecks(BuildProducer<TypeCheckExcludeBuildItem> excludes) {
// Exclude all checks that involve built-in value resolvers
// TODO we need a better way to exclude value resolvers that are not template extension methods
excludes.produce(new TypeCheckExcludeBuildItem(new Predicate<Check>() {
// TODO: We need a better way to exclude value resolvers that are not template extension methods
List<String> skipOperators = Arrays.asList("?:", "or", ":", "?", "&&", "||");

excludes.produce(new TypeCheckExcludeBuildItem(new Predicate<TypeCheck>() {
@Override
public boolean test(Check check) {
// RawString
if (check.isProperty() && check.nameIn("raw", "safe")) {
public boolean test(TypeCheck check) {
// RawString - these properties can be used on any object
if (check.isProperty() && ("raw".equals(check.name) || "safe".equals(check.name))) {
return true;
}
// Elvis and ternary operators
if (check.numberOfParameters == 1 && check.nameIn("?:", "or", ":", "?")) {
// Elvis, ternary and logical operators
if (check.numberOfParameters == 1 && skipOperators.contains(check.name)) {
return true;
}
// Collection.contains()
Expand Down Expand Up @@ -1390,7 +1393,7 @@ private void scan(Path root, Path directory, String basePath, BuildProducer<HotD
}
}

private static boolean isExcluded(Check check, List<TypeCheckExcludeBuildItem> excludes) {
private static boolean isExcluded(TypeCheck check, List<TypeCheckExcludeBuildItem> excludes) {
for (TypeCheckExcludeBuildItem exclude : excludes) {
if (exclude.getPredicate().test(check)) {
return true;
Expand Down
@@ -1,5 +1,6 @@
package io.quarkus.qute.deployment;

import java.util.Objects;
import java.util.function.Predicate;

import org.jboss.jandex.ClassInfo;
Expand All @@ -8,21 +9,29 @@
import io.quarkus.builder.item.MultiBuildItem;

/**
* Makes it possible to intentionally ignore some classes when performing type-safe checking.
* Makes it possible to intentionally ignore some parts of an expression when performing type-safe validation.
*
* @see TypeCheck
*/
public final class TypeCheckExcludeBuildItem extends MultiBuildItem {

private final Predicate<Check> predicate;
private final Predicate<TypeCheck> predicate;

public TypeCheckExcludeBuildItem(Predicate<Check> predicate) {
this.predicate = predicate;
public TypeCheckExcludeBuildItem(Predicate<TypeCheck> predicate) {
this.predicate = Objects.requireNonNull(predicate);
}

public Predicate<Check> getPredicate() {
public Predicate<TypeCheck> getPredicate() {
return predicate;
}

public static class Check {
/**
* Represents a type check of a part of an expression.
* <p>
* For example, the expression {@code item.name} has two parts, {@code item} and {@code name}, and for each part a new
* instance is created and tested for exclusion.
*/
public static class TypeCheck {

/**
* The name of a property/method, e.g. {@code foo} and {@code ping} for expression {@code foo.ping(bar)}.
Expand All @@ -39,7 +48,7 @@ public static class Check {
*/
public final int numberOfParameters;

public Check(String name, ClassInfo clazz, int parameters) {
public TypeCheck(String name, ClassInfo clazz, int parameters) {
this.name = name;
this.clazz = clazz;
this.numberOfParameters = parameters;
Expand Down
Expand Up @@ -27,6 +27,7 @@ public class ValidationSuccessTest {
// Built-in value resolvers
+ "{movie.name ?: 'Mono'} "
+ "{movie.alwaysTrue ? 'Mono' : 'Stereo'} "
+ "{movie.mainCharacters.size} "
// Name and number of params ok and param type ignored
+ "{movie.findService('foo')} "
// Name and number of params ok; name type ignored, age ok
Expand All @@ -47,7 +48,7 @@ public class ValidationSuccessTest {
@Test
public void testResult() {
// Validation succeeded! Yay!
assertEquals("Jason Jason Mono 10 11 ok 43 3 ohn",
assertEquals("Jason Jason Mono 1 10 11 ok 43 3 ohn",
movie.data("movie", new Movie("John")).data("name", "Vasik").data("surname", "Hu").data("age", 10l).render());
}

Expand Down
Expand Up @@ -73,6 +73,8 @@ public EngineProducer(QuteContext context, Event<EngineBuilder> builderReady, Ev
builder.addValueResolver(ValueResolvers.mapEntryResolver());
// foo.string.raw returns a RawString which is never escaped
builder.addValueResolver(ValueResolvers.rawResolver());
builder.addValueResolver(ValueResolvers.logicalAndResolver());
builder.addValueResolver(ValueResolvers.logicalOrResolver());

// Escape some characters for HTML templates
builder.addResultMapper(new HtmlEscaper());
Expand Down
@@ -1,13 +1,5 @@
package io.quarkus.qute;

import static io.quarkus.qute.ValueResolvers.collectionResolver;
import static io.quarkus.qute.ValueResolvers.mapEntryResolver;
import static io.quarkus.qute.ValueResolvers.mapResolver;
import static io.quarkus.qute.ValueResolvers.mapperResolver;
import static io.quarkus.qute.ValueResolvers.orResolver;
import static io.quarkus.qute.ValueResolvers.thisResolver;
import static io.quarkus.qute.ValueResolvers.trueResolver;

import java.io.Reader;
import java.util.ArrayList;
import java.util.HashMap;
Expand Down Expand Up @@ -87,8 +79,10 @@ public EngineBuilder addValueResolver(ValueResolver resolver) {
* @return self
*/
public EngineBuilder addDefaultValueResolvers() {
return addValueResolvers(mapResolver(), mapperResolver(), mapEntryResolver(), collectionResolver(),
thisResolver(), orResolver(), trueResolver());
return addValueResolvers(ValueResolvers.mapResolver(), ValueResolvers.mapperResolver(),
ValueResolvers.mapEntryResolver(), ValueResolvers.collectionResolver(),
ValueResolvers.thisResolver(), ValueResolvers.orResolver(), ValueResolvers.trueResolver(),
ValueResolvers.logicalAndResolver(), ValueResolvers.logicalOrResolver());
}

public EngineBuilder addDefaults() {
Expand Down
Expand Up @@ -8,6 +8,7 @@
import java.util.Map.Entry;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.function.Function;

/**
* Common value resolvers.
Expand Down Expand Up @@ -167,6 +168,64 @@ public CompletionStage<Object> resolve(EvalContext context) {
};
}

/**
* Performs conditional AND on the base object and the first parameter.
* It's a short-circuiting operation - the parameter is only evaluated if needed.
*
* @see Booleans#isFalsy(Object)
*/
public static ValueResolver logicalAndResolver() {
return new ValueResolver() {

public boolean appliesTo(EvalContext context) {
return context.getBase() != null && context.getParams().size() == 1
&& ("&&".equals(context.getName()));
}

@Override
public CompletionStage<Object> resolve(EvalContext context) {
boolean baseIsFalsy = Booleans.isFalsy(context.getBase());
return baseIsFalsy ? CompletableFuture.completedFuture(false)
: context.evaluate(context.getParams().get(0)).thenApply(new Function<Object, Object>() {
@Override
public Object apply(Object booleanParam) {
return !Booleans.isFalsy(booleanParam);
}
});
}

};
}

/**
* Performs conditional OR on the base object and the first parameter.
* It's a short-circuiting operation - the parameter is only evaluated if needed.
*
* @see Booleans#isFalsy(Object)
*/
public static ValueResolver logicalOrResolver() {
return new ValueResolver() {

public boolean appliesTo(EvalContext context) {
return context.getBase() != null && context.getParams().size() == 1
&& ("||".equals(context.getName()));
}

@Override
public CompletionStage<Object> resolve(EvalContext context) {
boolean baseIsFalsy = Booleans.isFalsy(context.getBase());
return !baseIsFalsy ? CompletableFuture.completedFuture(true)
: context.evaluate(context.getParams().get(0)).thenApply(new Function<Object, Object>() {
@Override
public Object apply(Object booleanParam) {
return !Booleans.isFalsy(booleanParam);
}
});
}

};
}

// helper methods

private static CompletionStage<Object> collectionResolveAsync(EvalContext context) {
Expand Down
@@ -1,5 +1,6 @@
package io.quarkus.qute;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

Expand Down Expand Up @@ -49,4 +50,18 @@ public void testIsFalsy() {
assertFalse(Booleans.isFalsy(new AtomicBoolean(true)));
}

@Test
public void testLogicalOperators() {
Engine engine = Engine.builder().addDefaults()
.addValueResolvers(ValueResolvers.logicalAndResolver(), ValueResolvers.logicalOrResolver())
.build();
assertEquals("true", engine.parse("{foo && bar}").data("foo", true).data("bar", 1).render());
assertEquals("true", engine.parse("{foo && bar && baz}").data("foo", true).data("bar", 1).data("baz", true).render());
assertEquals("false",
engine.parse("{foo && bar && baz}").data("foo", true).data("bar", 1).data("baz", false).render());
assertEquals("true", engine.parse("{foo || bar}").data("foo", true).data("bar", 0).render());
assertEquals("false", engine.parse("{foo || bar || baz}").data("foo", false).data("bar", 0)
.data("baz", Collections.emptyList()).render());
}

}

0 comments on commit 268d8bb

Please sign in to comment.