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 #8582
  • Loading branch information
mkouba committed Apr 15, 2020
1 parent fe07f6e commit bdad4e6
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 12 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package io.quarkus.qute;

import java.lang.reflect.Array;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.Map;

import io.quarkus.qute.Results.Result;

public final class Booleans {

private static final Long LONG_ZERO = 0L;
private static final Integer INTEGER_ZERO = 0;
private static final Short SHORT_ZERO = 0;
private static final Byte BYTE_ZERO = 0;
private static final Double DOUBLE_ZERO = 0.0;
private static final Float FLOAT_ZERO = 0.0F;

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;
}
return INTEGER_ZERO.equals(number) || LONG_ZERO.equals(number)
|| SHORT_ZERO.equals(number) || BYTE_ZERO.equals(number)
|| DOUBLE_ZERO.equals(number) || FLOAT_ZERO.equals(number);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
import io.quarkus.qute.Results.Result;
import io.quarkus.qute.SectionHelperFactory.ParserDelegate;
import io.quarkus.qute.SectionHelperFactory.SectionInitContext;

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

import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
Expand Down Expand Up @@ -51,13 +54,13 @@ private CompletionStage<ResultNode> resolveCondition(SectionResolutionContext co
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 CompletableFuture.completedFuture(ResultNode.NOOP);
} else {
return context.execute(block.block, context.resolutionContext());
}
});
}
Expand Down Expand Up @@ -218,7 +221,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 +303,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 +341,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,6 +1,9 @@
package io.quarkus.qute;

import io.quarkus.qute.Results.Result;

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

import java.util.Collection;
import java.util.Map;
import java.util.Map.Entry;
Expand Down Expand Up @@ -105,10 +108,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
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 bdad4e6

Please sign in to comment.