Skip to content

Commit

Permalink
Qute if section - consider truthy/falsy values during evaluation
Browse files Browse the repository at this point in the history
- resolves quarkusio#8582
  • Loading branch information
mkouba committed Apr 16, 2020
1 parent fe07f6e commit 3c031d8
Show file tree
Hide file tree
Showing 7 changed files with 176 additions and 19 deletions.
11 changes: 7 additions & 4 deletions docs/src/main/asciidoc/qute-reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,8 @@ This could be useful to access data for which the key is overriden:
|`{item.isActive ? item.name : 'Inactive item'}` outputs the value of `item.name` if `item.isActive` resolves to `true`.
|===

TIP: The condition in a ternary operator evaluates to `true` if the value is not considered `falsy` as described in the <<if_section>>.

NOTE: In fact, the operators are implemented as "virtual methods" that consume one parameter and can be used with infix notation, i.e. `{person.name or 'John'}` is translated to `{person.name.or('John')}`.

===== Character Escapes
Expand Down Expand Up @@ -374,8 +376,9 @@ The output will be:
[[if_section]]
===== If Section

A basic control flow section.
The simplest possible version accepts a single parameter and renders the content if it's evaluated to `true` (or `Boolean.TRUE`).
The `if` section represents a basic control flow section.
The simplest possible version accepts a single parameter and renders the content if the condition is evaluated to `true`.
A condition without an operator evaluates to `true` if the value is not considered `falsy`, i.e. if the value is not `null`, `false`, an empty collection, an empty map, an empty array, an empty string/char sequence or a number equal to zero.

[source]
----
Expand All @@ -384,7 +387,7 @@ The simplest possible version accepts a single parameter and renders the content
{/if}
----

You can also use the following operators:
You can also use the following operators in a condition:

|===
|Operator |Aliases |Precedence (higher wins)
Expand Down Expand Up @@ -456,7 +459,7 @@ Precedence rules can be overridden by parentheses.
----


You can add any number of `else` blocks:
You can also add any number of `else` blocks:

[source]
----
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package io.quarkus.qute;

import io.quarkus.qute.Results.Result;
import java.lang.reflect.Array;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Collection;
import java.util.Map;

public final class Booleans {

private Booleans() {
}

/**
* A value is considered falsy if it's null, {@link Result#NOT_FOUND}, {code false}, an empty collection, an empty map, an
* empty array, an empty string/char sequence or a number equal to zero.
*
* @param value
* @return {@code true} if the value is falsy
*/
public static boolean isFalsy(Object value) {
if (value == null || Results.Result.NOT_FOUND.equals(value)) {
return true;
} else if (value instanceof Boolean) {
return !(Boolean) value;
} else if (value instanceof Collection) {
return ((Collection<?>) value).isEmpty();
} else if (value instanceof Map) {
return ((Map<?, ?>) value).isEmpty();
} else if (value.getClass().isArray()) {
return Array.getLength(value) == 0;
} else if (value instanceof CharSequence) {
return ((CharSequence) value).length() == 0;
} else if (value instanceof Number) {
return isZero((Number) value);
}
return false;
}

private static boolean isZero(Number number) {
if (number instanceof BigDecimal) {
return BigDecimal.ZERO.compareTo((BigDecimal) number) == 0;
} else if (number instanceof BigInteger) {
return BigInteger.ZERO.equals(number);
}
if (number instanceof Float || number instanceof Double) {
return number.doubleValue() == 0.0;
}
// byte, short, integer, long and any other number
return number.longValue() == 0L;
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package io.quarkus.qute;

import static io.quarkus.qute.Booleans.isFalsy;

import io.quarkus.qute.Results.Result;
import io.quarkus.qute.SectionHelperFactory.ParserDelegate;
import io.quarkus.qute.SectionHelperFactory.SectionInitContext;
Expand Down Expand Up @@ -40,24 +42,34 @@ public class IfSectionHelper implements SectionHelper {

@Override
public CompletionStage<ResultNode> resolve(SectionResolutionContext context) {
return resolveCondition(context, blocks.iterator());
if (blocks.size() == 1) {
IfBlock block = blocks.get(0);
return block.condition.evaluate(context).thenCompose(r -> {
if (isFalsy(r)) {
return CompletableFuture.completedFuture(ResultNode.NOOP);
} else {
return context.execute(block.block, context.resolutionContext());
}
});
}
return resolveBlocks(context, blocks.iterator());
}

private CompletionStage<ResultNode> resolveCondition(SectionResolutionContext context,
private CompletionStage<ResultNode> resolveBlocks(SectionResolutionContext context,
Iterator<IfBlock> blocks) {
IfBlock block = blocks.next();
if (block.condition.isEmpty()) {
// else without operands
return context.execute(block.block, context.resolutionContext());
}
return block.condition.evaluate(context).thenCompose(r -> {
if (Boolean.TRUE.equals(r)) {
return context.execute(block.block, context.resolutionContext());
} else {
if (isFalsy(r)) {
if (blocks.hasNext()) {
return resolveCondition(context, blocks);
return resolveBlocks(context, blocks);
}
return CompletableFuture.completedFuture(ResultNode.NOOP);
} else {
return context.execute(block.block, context.resolutionContext());
}
});
}
Expand Down Expand Up @@ -218,7 +230,7 @@ CompletionStage<Object> evaluateNext(SectionResolutionContext context, Object va
} else {
Object val;
if (next.isLogicalComplement()) {
r = Boolean.TRUE.equals(r) ? Boolean.FALSE : Boolean.TRUE;
r = Booleans.isFalsy(r) ? Boolean.TRUE : Boolean.FALSE;
}
if (operator == null || !operator.isBinary()) {
val = r;
Expand Down Expand Up @@ -300,7 +312,7 @@ boolean evaluate(Object op1, Object op2) {
return compare(op1, op2);
case AND:
case OR:
return Boolean.TRUE.equals(op2);
return !isFalsy(op2);
default:
throw new TemplateException("Not a binary operator: " + this);
}
Expand Down Expand Up @@ -338,9 +350,9 @@ boolean compare(Object op1, Object op2) {
Boolean evaluate(Object op1) {
switch (this) {
case AND:
return Boolean.TRUE.equals(op1) ? null : Boolean.FALSE;
return isFalsy(op1) ? Boolean.FALSE : null;
case OR:
return Boolean.TRUE.equals(op1) ? Boolean.TRUE : null;
return isFalsy(op1) ? null : Boolean.TRUE;
default:
throw new TemplateException("Not a short-circuiting operator: " + this);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package io.quarkus.qute;

import static io.quarkus.qute.Booleans.isFalsy;

import io.quarkus.qute.Results.Result;
import java.util.Collection;
import java.util.Map;
Expand Down Expand Up @@ -105,10 +107,10 @@ public boolean appliesTo(EvalContext context) {

@Override
public CompletionStage<Object> resolve(EvalContext context) {
if (Boolean.TRUE.equals(context.getBase())) {
return context.evaluate(context.getParams().get(0));
if (isFalsy(context.getBase())) {
return Results.NOT_FOUND;
}
return Results.NOT_FOUND;
return context.evaluate(context.getParams().get(0));
}

};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package io.quarkus.qute;

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

import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import org.junit.jupiter.api.Test;

public class BooleansTest {

@Test
public void testIsFalsy() {
assertTrue(Booleans.isFalsy(null));
assertTrue(Booleans.isFalsy(false));
assertTrue(Booleans.isFalsy(""));
assertTrue(Booleans.isFalsy(new StringBuffer()));
assertTrue(Booleans.isFalsy(0f));
assertTrue(Booleans.isFalsy(0));
assertTrue(Booleans.isFalsy(0.0));
assertTrue(Booleans.isFalsy(new BigDecimal("0.0")));
assertTrue(Booleans.isFalsy(new BigInteger("0")));
assertTrue(Booleans.isFalsy(new ArrayList<>()));
assertTrue(Booleans.isFalsy(new HashSet<>()));
assertTrue(Booleans.isFalsy(new HashMap<>()));
assertTrue(Booleans.isFalsy(new String[0]));
// truthy values
assertFalse(Booleans.isFalsy(new Object()));
assertFalse(Booleans.isFalsy(true));
assertFalse(Booleans.isFalsy("foo"));
assertFalse(Booleans.isFalsy(new StringBuffer().append("foo")));
assertFalse(Booleans.isFalsy(1f));
assertFalse(Booleans.isFalsy(4));
assertFalse(Booleans.isFalsy(0.2));
assertFalse(Booleans.isFalsy(new BigDecimal("0.1")));
assertFalse(Booleans.isFalsy(new BigInteger("10")));
assertFalse(Booleans.isFalsy(Collections.singletonList("foo")));
assertFalse(Booleans.isFalsy(Collections.singletonMap("foo", "bar")));
assertFalse(Booleans.isFalsy(new String[] { "foo" }));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import io.quarkus.qute.IfSectionHelper.Operator;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand All @@ -28,7 +29,7 @@ public void tesIfElse() {
}

@Test
public void tesIfOperator() {
public void testIfOperator() {
Engine engine = Engine.builder().addDefaults().build();

Map<String, Object> data = new HashMap<>();
Expand All @@ -43,7 +44,7 @@ public void tesIfOperator() {
assertEquals("OK", engine.parse("{#if one >= one}OK{/if}").render(data));
assertEquals("OK", engine.parse("{#if one >= 0}OK{/if}").render(data));
assertEquals("OK", engine.parse("{#if one == one}OK{/if}").render(data));
assertEquals("OK", engine.parse("{#if one}NOK{#else if name eq foo}OK{/if}").render(data));
assertEquals("OK", engine.parse("{#if one is 2}NOK{#else if name eq foo}OK{/if}").render(data));
assertEquals("OK", engine.parse("{#if name is foo}OK{/if}").render(data));
assertEquals("OK", engine.parse("{#if two is 2}OK{/if}").render(data));
assertEquals("OK", engine.parse("{#if name != null}OK{/if}").render(data));
Expand Down Expand Up @@ -116,6 +117,43 @@ public void testParameterParsing() {
assertEquals("true", params.get(2));
}

@Test
public void testFalsy() {
Engine engine = Engine.builder().addDefaults().build();

Map<String, Object> data = new HashMap<>();
data.put("name", "foo");
data.put("nameEmpty", "");
data.put("boolTrue", true);
data.put("boolFalse", false);
data.put("intTwo", Integer.valueOf(2));
data.put("intZero", Integer.valueOf(0));
data.put("list", Collections.singleton("foo"));
data.put("setEmpty", Collections.emptySet());
data.put("mapEmpty", Collections.emptyMap());
data.put("array", new String[] { "foo" });
data.put("arrayEmpty", new String[] {});

assertEquals("1", engine.parse("{#if name}1{#else}0{/if}").render(data));
assertEquals("0", engine.parse("{#if nameEmpty}1{#else}0{/if}").render(data));
assertEquals("1", engine.parse("{#if boolTrue}1{#else}0{/if}").render(data));
assertEquals("0", engine.parse("{#if boolFalse}1{#else}0{/if}").render(data));
assertEquals("1", engine.parse("{#if intTwo}1{#else}0{/if}").render(data));
assertEquals("0", engine.parse("{#if intZero}1{#else}0{/if}").render(data));
assertEquals("1", engine.parse("{#if list}1{#else}0{/if}").render(data));
assertEquals("0", engine.parse("{#if setEmpty}1{#else}0{/if}").render(data));
assertEquals("0", engine.parse("{#if mapEmpty}1{#else}0{/if}").render(data));
assertEquals("1", engine.parse("{#if array}1{#else}0{/if}").render(data));
assertEquals("0", engine.parse("{#if arrayEmpty}1{#else}0{/if}").render(data));
assertEquals("1", engine.parse("{#if !arrayEmpty}1{#else}0{/if}").render(data));
assertEquals("1", engine.parse("{#if arrayEmpty || name}1{#else}0{/if}").render(data));
assertEquals("0", engine.parse("{#if arrayEmpty && name}1{#else}0{/if}").render(data));
assertEquals("1", engine.parse("{#if array && intTwo}1{#else}0{/if}").render(data));
assertEquals("1", engine.parse("{#if (array && intZero) || true}1{#else}0{/if}").render(data));
assertEquals("0", engine.parse("{#if nonExistent}1{#else}0{/if}").render(data));
assertEquals("1", engine.parse("{#if !nonExistent}1{#else}0{/if}").render(data));
}

private void assertParserError(String template, String message, int line) {
Engine engine = Engine.builder().addDefaultSectionHelpers().build();
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ public void testTernaryOperator() {
Template template = engine
.parse("{name ? 'Name true' : 'Name false'}. {surname ? 'Surname true' : foo}.");
assertEquals("Name true. baz.", template.data("name", true).data("foo", "baz").render());

assertEquals("1", engine.parse("{name ? 1 : 2}").data("name", "foo").render());
}

@Test
Expand Down

0 comments on commit 3c031d8

Please sign in to comment.