Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Qute if section - consider truthy/falsy values during evaluation #8602

Merged
merged 1 commit into from
Apr 16, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,59 @@
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;
import java.util.concurrent.atomic.AtomicBoolean;

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 AtomicBoolean) {
return !((AtomicBoolean) value).get();
} 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;
}
if (number instanceof Byte || number instanceof Short || number instanceof Integer || number instanceof Long) {
return number.longValue() == 0L;
}
return BigDecimal.ZERO.compareTo(new BigDecimal(number.toString())) == 0;
}

}
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,52 @@
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 java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
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]));
assertTrue(Booleans.isFalsy(new AtomicLong(0)));
assertTrue(Booleans.isFalsy(new AtomicBoolean(false)));
// 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" }));
assertFalse(Booleans.isFalsy(new AtomicLong(10)));
assertFalse(Booleans.isFalsy(new AtomicBoolean(true)));
}

}
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