Skip to content

Commit

Permalink
Merge pull request #510 from HubSpot/exp-test-safe-v3
Browse files Browse the repository at this point in the history
Expression test parity v3
  • Loading branch information
mattcoley committed Sep 24, 2020
2 parents 5becac4 + 781e71f commit 866fb5c
Show file tree
Hide file tree
Showing 36 changed files with 985 additions and 49 deletions.
32 changes: 30 additions & 2 deletions src/main/java/com/hubspot/jinjava/el/ext/ExtendedParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,23 @@
import static de.odysseus.el.tree.impl.Builder.Feature.NULL_PROPERTIES;
import static de.odysseus.el.tree.impl.Scanner.Symbol.COLON;
import static de.odysseus.el.tree.impl.Scanner.Symbol.COMMA;
import static de.odysseus.el.tree.impl.Scanner.Symbol.EQ;
import static de.odysseus.el.tree.impl.Scanner.Symbol.FALSE;
import static de.odysseus.el.tree.impl.Scanner.Symbol.GE;
import static de.odysseus.el.tree.impl.Scanner.Symbol.GT;
import static de.odysseus.el.tree.impl.Scanner.Symbol.IDENTIFIER;
import static de.odysseus.el.tree.impl.Scanner.Symbol.LBRACK;
import static de.odysseus.el.tree.impl.Scanner.Symbol.LE;
import static de.odysseus.el.tree.impl.Scanner.Symbol.LPAREN;
import static de.odysseus.el.tree.impl.Scanner.Symbol.LT;
import static de.odysseus.el.tree.impl.Scanner.Symbol.NE;
import static de.odysseus.el.tree.impl.Scanner.Symbol.QUESTION;
import static de.odysseus.el.tree.impl.Scanner.Symbol.RBRACK;
import static de.odysseus.el.tree.impl.Scanner.Symbol.RPAREN;
import static de.odysseus.el.tree.impl.Scanner.Symbol.TRUE;

import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import de.odysseus.el.tree.impl.Builder;
import de.odysseus.el.tree.impl.Builder.Feature;
import de.odysseus.el.tree.impl.Parser;
Expand All @@ -33,6 +42,7 @@
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.el.ELException;

public class ExtendedParser extends Parser {
Expand All @@ -53,6 +63,19 @@ public class ExtendedParser extends Parser {
static final Scanner.ExtensionToken TRUNC_DIV = TruncDivOperator.TOKEN;
static final Scanner.ExtensionToken POWER_OF = PowerOfOperator.TOKEN;

static final Set<Symbol> VALID_SYMBOLS_FOR_EXP_TEST = Sets.newHashSet(
IDENTIFIER,
EQ,
NE,
LT,
LE,
GT,
GE,
TRUE,
FALSE,
CollectionMembershipOperator.TOKEN.getSymbol()
);

static {
ExtendedScanner.addKeyToken(IF);
ExtendedScanner.addKeyToken(ELSE);
Expand Down Expand Up @@ -396,13 +419,14 @@ protected AstNode value() throws ScanException, ParseException {
} else if (
"is".equals(getToken().getImage()) &&
"not".equals(lookahead(0).getImage()) &&
lookahead(1).getSymbol() == IDENTIFIER
isPossibleExpTest(lookahead(1).getSymbol())
) {
consumeToken(); // 'is'
consumeToken(); // 'not'
v = buildAstMethodForIdentifier(v, "evaluateNegated");
} else if (
"is".equals(getToken().getImage()) && lookahead(0).getSymbol() == IDENTIFIER
"is".equals(getToken().getImage()) &&
isPossibleExpTest(lookahead(0).getSymbol())
) {
consumeToken(); // 'is'
v = buildAstMethodForIdentifier(v, "evaluate");
Expand All @@ -413,6 +437,10 @@ protected AstNode value() throws ScanException, ParseException {
}
}

private boolean isPossibleExpTest(Symbol symbol) {
return VALID_SYMBOLS_FOR_EXP_TEST.contains(symbol);
}

private AstNode buildAstMethodForIdentifier(AstNode astNode, String property)
throws ScanException, ParseException {
String exptestName = consumeToken().getImage();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public enum InvalidReason {
JSON_WRITE("object could not be written as a string"),
REGEX("with value %s must be valid regex"),
POSITIVE_NUMBER("with value %s must be a positive number"),
NOT_ITERABLE("with value '%s' must be iterable"),
NON_ZERO_NUMBER("with value %s must be non-zero"),
NULL_IN_LIST("of type 'list' cannot contain a null item"),
NULL_ATTRIBUTE_IN_LIST(
Expand Down
20 changes: 20 additions & 0 deletions src/main/java/com/hubspot/jinjava/lib/exptest/ExpTestLibrary.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,20 @@ protected void registerDefaults() {
IsDefinedExpTest.class,
IsDivisibleByExpTest.class,
IsEqualToExpTest.class,
IsEqExpTest.class,
IsEqualsSymbolExpTest.class,
IsNeExpTest.class,
IsNotEqualToSymbolExpTest.class,
IsLtTest.class,
IsLessThanExpTest.class,
IsLessThanSymbolExpTest.class,
IsLeTest.class,
IsLessThanOrEqualToSymbolExpTest.class,
IsGtTest.class,
IsGreaterThanExpTest.class,
IsGreaterThanSymbolExpTest.class,
IsGeTest.class,
IsGreaterThanOrEqualToSymbolExpTest.class,
IsEvenExpTest.class,
IsIterableExpTest.class,
IsLowerExpTest.class,
Expand All @@ -24,14 +38,20 @@ protected void registerDefaults() {
IsOddExpTest.class,
IsSameAsExpTest.class,
IsSequenceExpTest.class,
IsBooleanExpTest.class,
IsIntegerExpTest.class,
IsFloatExpTest.class,
IsStringExpTest.class,
IsStringContainingExpTest.class,
IsStringStartingWithExpTest.class,
IsTrueExpTest.class,
IsFalseExpTest.class,
IsTruthyExpTest.class,
IsUndefinedExpTest.class,
IsUpperExpTest.class,
IsContainingAllExpTest.class,
IsContainingExpTest.class,
IsInExpTest.class,
IsWithinExpTest.class
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.hubspot.jinjava.lib.exptest;

import com.hubspot.jinjava.doc.annotations.JinjavaDoc;
import com.hubspot.jinjava.doc.annotations.JinjavaParam;
import com.hubspot.jinjava.doc.annotations.JinjavaSnippet;
import com.hubspot.jinjava.interpret.JinjavaInterpreter;

@JinjavaDoc(
value = "Return true if object is a boolean (in a strict sense, not in its ability to evaluate to a truthy expression)",
input = @JinjavaParam(value = "value", type = "object", required = true),
snippets = {
@JinjavaSnippet(
code = "{% if true is boolean %}\n" +
" <!--this code will always render-->\n" +
"{% endif %}"
)
}
)
public class IsBooleanExpTest implements ExpTest {

@Override
public String getName() {
return "boolean";
}

@Override
public boolean evaluate(Object var, JinjavaInterpreter interpreter, Object... args) {
return var instanceof Boolean;
}
}
12 changes: 12 additions & 0 deletions src/main/java/com/hubspot/jinjava/lib/exptest/IsEqExpTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.hubspot.jinjava.lib.exptest;

import com.hubspot.jinjava.doc.annotations.JinjavaDoc;

@JinjavaDoc(value = "", aliasOf = "equalto")
public class IsEqExpTest extends IsEqualToExpTest {

@Override
public String getName() {
return "eq";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.hubspot.jinjava.lib.exptest;

import com.hubspot.jinjava.doc.annotations.JinjavaDoc;

@JinjavaDoc(value = "", aliasOf = "equalto")
public class IsEqualsSymbolExpTest extends IsEqualToExpTest {

@Override
public String getName() {
return "==";
}
}
30 changes: 30 additions & 0 deletions src/main/java/com/hubspot/jinjava/lib/exptest/IsFalseExpTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.hubspot.jinjava.lib.exptest;

import com.hubspot.jinjava.doc.annotations.JinjavaDoc;
import com.hubspot.jinjava.doc.annotations.JinjavaParam;
import com.hubspot.jinjava.doc.annotations.JinjavaSnippet;
import com.hubspot.jinjava.interpret.JinjavaInterpreter;

@JinjavaDoc(
value = "Return true if object is a boolean and false",
input = @JinjavaParam(value = "value", type = "object", required = true),
snippets = {
@JinjavaSnippet(
code = "{% if false is false %}\n" +
" <!--this code will always render-->\n" +
"{% endif %}"
)
}
)
public class IsFalseExpTest implements ExpTest {

@Override
public String getName() {
return "false";
}

@Override
public boolean evaluate(Object var, JinjavaInterpreter interpreter, Object... args) {
return var instanceof Boolean && !(Boolean) var;
}
}
35 changes: 35 additions & 0 deletions src/main/java/com/hubspot/jinjava/lib/exptest/IsFloatExpTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.hubspot.jinjava.lib.exptest;

import com.hubspot.jinjava.doc.annotations.JinjavaDoc;
import com.hubspot.jinjava.doc.annotations.JinjavaParam;
import com.hubspot.jinjava.doc.annotations.JinjavaSnippet;
import com.hubspot.jinjava.interpret.JinjavaInterpreter;
import java.math.BigDecimal;

@JinjavaDoc(
value = "Return true if object is a float",
input = @JinjavaParam(value = "value", type = "object", required = true),
snippets = {
@JinjavaSnippet(
code = "{% if num is float %}\n" +
" <!--code to render if num contains an floating point value-->\n" +
"{% endif %}"
)
}
)
public class IsFloatExpTest implements ExpTest {

@Override
public String getName() {
return "float";
}

@Override
public boolean evaluate(Object var, JinjavaInterpreter interpreter, Object... args) {
return (
var instanceof Double ||
var instanceof Float ||
(var instanceof BigDecimal && ((BigDecimal) var).scale() > 0)
);
}
}
55 changes: 55 additions & 0 deletions src/main/java/com/hubspot/jinjava/lib/exptest/IsGeTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.hubspot.jinjava.lib.exptest;

import com.hubspot.jinjava.doc.annotations.JinjavaDoc;
import com.hubspot.jinjava.doc.annotations.JinjavaParam;
import com.hubspot.jinjava.doc.annotations.JinjavaSnippet;
import com.hubspot.jinjava.el.TruthyTypeConverter;
import com.hubspot.jinjava.interpret.JinjavaInterpreter;
import com.hubspot.jinjava.interpret.TemplateSyntaxException;
import de.odysseus.el.misc.BooleanOperations;
import de.odysseus.el.misc.TypeConverter;

@JinjavaDoc(
value = "Returns true if the first object's value is greater than or equal to the second object's value",
input = @JinjavaParam(value = "first", type = "object", required = true),
params = {
@JinjavaParam(
value = "other",
type = "object",
desc = "Another object to compare against",
required = true
)
},
snippets = {
@JinjavaSnippet(
code = "{% if foo.expression is ge 42 %}\n" +
" the foo attribute evaluates to the constant 42\n" +
"{% endif %}\n"
),
@JinjavaSnippet(
desc = "Usage with the selectattr filter",
code = "{{ users|selectattr(\"num\", \"ge\", \"2\") }}"
)
}
)
public class IsGeTest implements ExpTest {
private static final TypeConverter TYPE_CONVERTER = new TruthyTypeConverter();

@Override
public String getName() {
return "ge";
}

@Override
public boolean evaluate(Object var, JinjavaInterpreter interpreter, Object... args) {
if (args.length == 0) {
throw new TemplateSyntaxException(
interpreter,
getName(),
"requires 1 argument (other object to compare against)"
);
}

return BooleanOperations.ge(TYPE_CONVERTER, var, args[0]);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.hubspot.jinjava.lib.exptest;

import com.hubspot.jinjava.doc.annotations.JinjavaDoc;

@JinjavaDoc(value = "", aliasOf = "gt")
public class IsGreaterThanExpTest extends IsGtTest {

@Override
public String getName() {
return "greaterthan";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.hubspot.jinjava.lib.exptest;

import com.hubspot.jinjava.doc.annotations.JinjavaDoc;

@JinjavaDoc(value = "", aliasOf = "ge")
public class IsGreaterThanOrEqualToSymbolExpTest extends IsGeTest {

@Override
public String getName() {
return ">=";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.hubspot.jinjava.lib.exptest;

import com.hubspot.jinjava.doc.annotations.JinjavaDoc;

@JinjavaDoc(value = "", aliasOf = "gt")
public class IsGreaterThanSymbolExpTest extends IsGtTest {

@Override
public String getName() {
return ">";
}
}
Loading

0 comments on commit 866fb5c

Please sign in to comment.