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

Filter upgrades to support kw params #531

Merged
merged 7 commits into from
Nov 16, 2020
95 changes: 68 additions & 27 deletions src/main/java/com/hubspot/jinjava/lib/filter/AbstractFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
import com.hubspot.jinjava.doc.annotations.JinjavaParam;
import com.hubspot.jinjava.interpret.InvalidInputException;
import com.hubspot.jinjava.interpret.JinjavaInterpreter;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
Expand All @@ -29,6 +28,7 @@
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import javax.annotation.Nullable;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
Expand All @@ -40,7 +40,7 @@
* @see JinjavaDoc
* @see JinjavaParam
*/
public abstract class AbstractFilter implements Filter {
public abstract class AbstractFilter implements Filter, AdvancedFilter {
private static final Map<Class, Map<String, JinjavaParam>> NAMED_ARGUMENTS_CACHE = new ConcurrentHashMap<>();
private static final Map<Class, Map<String, Object>> DEFAULT_VALUES_CACHE = new ConcurrentHashMap<>();

Expand Down Expand Up @@ -119,50 +119,77 @@ public Object filter(
}

protected Object parseArg(
JinjavaInterpreter interpreter,
@Nullable JinjavaInterpreter interpreter,
JinjavaParam jinjavaParamMetadata,
Object value
) {
if (
jinjavaParamMetadata.type() == null ||
value == null ||
Arrays.asList("object", "dict", "sequence").contains(jinjavaParamMetadata.type())
) {
if (jinjavaParamMetadata.type() == null || value == null) {
return value;
}
String valueString = Objects.toString(value, null);
switch (jinjavaParamMetadata.type().toLowerCase()) {
case "boolean":
return value instanceof Boolean
? (Boolean) value
: BooleanUtils.toBooleanObject(valueString);
: BooleanUtils.toBooleanObject(value.toString());
case "int":
return value instanceof Integer
? (Integer) value
: NumberUtils.toInt(valueString);
return parseNumber(value, int.class);
case "long":
return value instanceof Long ? (Long) value : NumberUtils.toLong(valueString);
return parseNumber(value, long.class);
case "float":
return value instanceof Float ? (Float) value : NumberUtils.toFloat(valueString);
return parseNumber(value, float.class);
case "double":
return value instanceof Double
? (Double) value
: NumberUtils.toDouble(valueString);
return parseNumber(value, double.class);
case "number":
return value instanceof Number ? (Number) value : new BigDecimal(valueString);
return parseNumber(value, Number.class);
case "string":
return valueString;
return value.toString();
case "object":
case "dict":
case "sequence":
return value;
default:
throw new InvalidInputException(
interpreter,
"INVALID_ARG_NAME",
String errorMessage = String.format(
"Argument named '%s' with value '%s' cannot be parsed for filter '%s'",
jinjavaParamMetadata.value(),
value,
getName()
);
if (interpreter != null) { //Filter runtime vs init
throw new InvalidInputException(interpreter, "INVALID_ARG_NAME", errorMessage);
} else {
throw new IllegalArgumentException(errorMessage);
}
}
}

public <N extends Number> Number parseNumber(Object value, Class<N> numberClass) {
//check if needs to be parsed to number first, and then convert to required type
if (!(value instanceof Number)) {
String str = Objects.toString(value, null);
if (NumberUtils.isCreatable(str)) {
value = NumberUtils.createNumber(str);
} else {
throw new IllegalArgumentException(
String.format(
"Argument named '%s' with value '%s' cannot be parsed for filter %s",
jinjavaParamMetadata.value(),
"Input '%s' is not parsable type of '%s' for filter '%s'",
value,
numberClass,
getName()
)
);
}
}
Number n = (Number) value;
if (numberClass == int.class || numberClass == Integer.class) {
return n.intValue();
} else if (numberClass == long.class || numberClass == Long.class) {
return n.longValue();
} else if (numberClass == float.class || numberClass == Float.class) {
return n.floatValue();
} else if (numberClass == double.class || numberClass == Double.class) {
return n.doubleValue();
} else { //if nclass == Number.class
return n;
}
}

Expand All @@ -171,7 +198,13 @@ public void validateArgs(
Map<String, Object> parsedArgs
) {
for (JinjavaParam jinjavaParam : namedArguments.values()) {
if (jinjavaParam.required() && !parsedArgs.containsKey(jinjavaParam.value())) {
if (
jinjavaParam.required() &&
(
!parsedArgs.containsKey(jinjavaParam.value()) ||
parsedArgs.get(jinjavaParam.value()) == null
)
) {
throw new InvalidInputException(
interpreter,
"MISSING_REQUIRED_ARG",
Expand Down Expand Up @@ -206,6 +239,10 @@ public String getIndexedArgumentName(int position) {
.orElse(null);
}

public Object getDefaultValue(String argName) {
return defaultValues.get(argName);
}

public Map<String, JinjavaParam> initNamedArguments() {
JinjavaDoc jinjavaDoc = this.getClass().getAnnotation(JinjavaDoc.class);
if (jinjavaDoc != null) {
Expand Down Expand Up @@ -235,7 +272,11 @@ public Map<String, Object> initDefaultValues() {
.stream()
.filter(e -> StringUtils.isNotEmpty(e.getValue().defaultValue()))
.collect(
ImmutableMap.toImmutableMap(Map.Entry::getKey, e -> e.getValue().defaultValue())
// ImmutableMap.toImmutableMap(Map.Entry::getKey, e -> e.getValue().defaultValue())
ImmutableMap.toImmutableMap(
Map.Entry::getKey,
e -> parseArg(null, e.getValue(), e.getValue().defaultValue())
)
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.hubspot.jinjava.interpret.JinjavaInterpreter;
import com.hubspot.jinjava.lib.fn.Functions;
import com.hubspot.jinjava.objects.date.StrftimeFormatter;
import java.util.Map;

@JinjavaDoc(
value = "Formats a date object",
Expand All @@ -17,19 +18,19 @@
),
params = {
@JinjavaParam(
value = "format",
value = DateTimeFormatFilter.FORMAT_PARAM,
defaultValue = StrftimeFormatter.DEFAULT_DATE_FORMAT,
desc = "The format of the date determined by the directives added to this parameter"
),
@JinjavaParam(
value = "timezone",
defaultValue = "utc",
value = DateTimeFormatFilter.TIMEZONE_PARAM,
defaultValue = "UTC",
desc = "Time zone of output date"
),
@JinjavaParam(
value = "locale",
value = DateTimeFormatFilter.LOCALE_PARAM,
type = "string",
defaultValue = "us",
defaultValue = "en-US",
desc = "The language code to use when formatting the datetime"
)
},
Expand All @@ -40,19 +41,29 @@
)
}
)
public class DateTimeFormatFilter implements Filter {
public class DateTimeFormatFilter extends AbstractFilter implements Filter {
public static final String FORMAT_PARAM = "format";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really appreciate the static constants!

public static final String TIMEZONE_PARAM = "timezone";
public static final String LOCALE_PARAM = "locale";

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

@Override
public Object filter(Object var, JinjavaInterpreter interpreter, String... args) {
if (args.length > 0) {
return Functions.dateTimeFormat(var, args);
} else {
public Object filter(
Object var,
JinjavaInterpreter interpreter,
Map<String, Object> parsedArgs
) {
String format = (String) parsedArgs.get(FORMAT_PARAM);
String timezone = (String) parsedArgs.get(TIMEZONE_PARAM);
String locale = (String) parsedArgs.get(LOCALE_PARAM);
if (format == null && timezone == null && locale == null) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Must all of these be null?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could probably fix up the Functions code, but currently it errors out on zone being null. Let me know if you are happy for me to change it

return Functions.dateTimeFormat(var);
} else {
return Functions.dateTimeFormat(var, format, timezone, locale);
}
}
}
25 changes: 13 additions & 12 deletions src/main/java/com/hubspot/jinjava/lib/filter/JoinFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,20 @@
import com.hubspot.jinjava.util.ForLoop;
import com.hubspot.jinjava.util.LengthLimitingStringBuilder;
import com.hubspot.jinjava.util.ObjectIterator;
import java.util.Map;
import java.util.Objects;

@JinjavaDoc(
value = "Return a string which is the concatenation of the strings in the sequence.",
input = @JinjavaParam(value = "value", desc = "The values to join", required = true),
params = {
@JinjavaParam(
value = "d",
value = JoinFilter.SEPARATOR_PARAM,
desc = "The separator string used to join the items",
defaultValue = "(empty String)"
),
@JinjavaParam(
value = "attr",
value = JoinFilter.ATTRIBUTE_PARAM,
desc = "Optional dict object attribute to use in joining"
)
},
Expand All @@ -37,28 +38,28 @@
)
}
)
public class JoinFilter implements Filter {
public class JoinFilter extends AbstractFilter implements Filter {
public static final String SEPARATOR_PARAM = "d";
public static final String ATTRIBUTE_PARAM = "attribute";

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

@Override
public Object filter(Object var, JinjavaInterpreter interpreter, String... args) {
public Object filter(
Object var,
JinjavaInterpreter interpreter,
Map<String, Object> parsedArgs
) {
LengthLimitingStringBuilder stringBuilder = new LengthLimitingStringBuilder(
interpreter.getConfig().getMaxStringLength()
);

String separator = "";
if (args.length > 0) {
separator = args[0];
}
String separator = (String) parsedArgs.get(SEPARATOR_PARAM);

String attr = null;
if (args.length > 1) {
attr = args[1];
}
String attr = (String) parsedArgs.get(ATTRIBUTE_PARAM);

ForLoop loop = ObjectIterator.getLoop(var);
boolean first = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import com.hubspot.jinjava.interpret.InvalidInputException;
import com.hubspot.jinjava.interpret.InvalidReason;
import com.hubspot.jinjava.interpret.JinjavaInterpreter;
import com.hubspot.jinjava.interpret.TemplateSyntaxException;
import java.util.Map;

@JinjavaDoc(
value = "Return a copy of the value with all occurrences of a matched regular expression (Java RE2 syntax) " +
Expand All @@ -23,12 +23,12 @@
),
params = {
@JinjavaParam(
value = "regex",
value = RegexReplaceFilter.REGEX_KEY,
desc = "The regular expression that you want to match and replace",
required = true
),
@JinjavaParam(
value = "new",
value = RegexReplaceFilter.REPLACE_WITH,
desc = "The new string that you replace the matched substring",
required = true
)
Expand All @@ -40,39 +40,29 @@
)
}
)
public class RegexReplaceFilter implements Filter {
public class RegexReplaceFilter extends AbstractFilter {
public static final String REGEX_KEY = "regex";
public static final String REPLACE_WITH = "new";

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

@Override
public Object filter(Object var, JinjavaInterpreter interpreter, String... args) {
if (args.length < 2) {
throw new TemplateSyntaxException(
interpreter,
getName(),
"requires 2 arguments (regex string, replacement string)"
);
}

if (args[0] == null || args[1] == null) {
throw new TemplateSyntaxException(
interpreter,
getName(),
"requires both a valid regex and new params (not null)"
);
}

public Object filter(
Object var,
JinjavaInterpreter interpreter,
Map<String, Object> parsedArgs
) {
if (var == null) {
return null;
}

if (var instanceof String) {
String s = (String) var;
String toReplace = args[0];
String replaceWith = args[1];
String toReplace = (String) parsedArgs.get(REGEX_KEY);
String replaceWith = (String) parsedArgs.get(REPLACE_WITH);

try {
Pattern p = Pattern.compile(toReplace);
Expand Down
Loading