Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ protected void registerDefaults() {
IsFloatExpTest.class,
IsStringExpTest.class,
IsStringContainingExpTest.class,
IsStringMatchingRegexExpTest.class,
IsStringStartingWithExpTest.class,
IsTrueExpTest.class,
IsFalseExpTest.class,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package com.hubspot.jinjava.lib.exptest;

import com.google.re2j.Matcher;
import com.google.re2j.Pattern;
import com.google.re2j.PatternSyntaxException;
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.InvalidArgumentException;
import com.hubspot.jinjava.interpret.InvalidReason;
import com.hubspot.jinjava.interpret.JinjavaInterpreter;
import com.hubspot.jinjava.interpret.TemplateSyntaxException;

@JinjavaDoc(
value = "Return true if object is a string which matches a specified regular expression " +
"(Java RE2 syntax). Uses partial matching (the pattern can match anywhere in the string). " +
"Anchor with ^...$ for full-string matching.",
input = @JinjavaParam(value = "string", type = "string", required = true),
params = @JinjavaParam(
value = "regex",
type = "string",
desc = "The regular expression to match against the string",
required = true
),
snippets = {
@JinjavaSnippet(
code = "{% if variable is string_matching_regex '[0-9]+' %}\n" +
" <!--code to render if variable matches regex -->\n" +
"{% endif %}"
),
@JinjavaSnippet(
desc = "Use with selectattr to filter a list by regex",
code = "{{ items|selectattr('name', 'string_matching_regex', '^foo') }}"
),
}
)
public class IsStringMatchingRegexExpTest extends IsStringExpTest {

@Override
public String getName() {
return super.getName() + "_matching_regex";
}

@Override
public boolean evaluate(Object var, JinjavaInterpreter interpreter, Object... args) {
if (!super.evaluate(var, interpreter, args)) {
return false;
}

if (args.length == 0) {
throw new TemplateSyntaxException(
interpreter,
getName(),
"requires 1 argument (regex string)"
);
}

if (args[0] == null) {
return false;
}

String regex = args[0].toString();

try {
Pattern p = Pattern.compile(regex);
Matcher matcher = p.matcher((String) var);

return matcher.find();
} catch (PatternSyntaxException e) {
throw new InvalidArgumentException(
interpreter,
this,
InvalidReason.REGEX,
0,
regex
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ protected void registerDefaults() {
PlusTimeFilter.class,
PrettyPrintFilter.class,
RandomFilter.class,
MatchesRegexFilter.class,
RegexReplaceFilter.class,
RejectFilter.class,
RejectAttrFilter.class,
Expand Down
108 changes: 108 additions & 0 deletions src/main/java/com/hubspot/jinjava/lib/filter/MatchesRegexFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package com.hubspot.jinjava.lib.filter;

import static com.hubspot.jinjava.lib.filter.ReplaceFilter.checkLength;

import com.google.re2j.Matcher;
import com.google.re2j.Pattern;
import com.google.re2j.PatternSyntaxException;
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.InvalidArgumentException;
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 com.hubspot.jinjava.objects.SafeString;

@JinjavaDoc(
value = "Returns true if the value matches the given regular expression (Java RE2 syntax), " +
"false otherwise. Uses partial matching (the pattern can match anywhere in the string). " +
"Anchor with ^...$ for full-string matching.",
input = @JinjavaParam(
value = "s",
desc = "Base string to test against",
required = true
),
params = {
@JinjavaParam(
value = "regex",
desc = "The regular expression to match against the string",
required = true
),
},
snippets = {
@JinjavaSnippet(
code = "{% if \"It costs $300\"|matches_regex(\"[0-9]+\") %}\n" +
" Contains a number\n" +
"{% endif %}"
),
@JinjavaSnippet(
desc = "Anchor the pattern to match the entire string",
code = "{% if \"hello\"|matches_regex(\"^[a-z]+$\") %}\n" +
" All lowercase letters\n" +
"{% endif %}"
),
}
)
public class MatchesRegexFilter implements Filter {

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

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

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

if (var == null) {
return false;
}

String s;
if (var instanceof String) {
s = (String) var;
} else if (var instanceof SafeString) {
s = ((SafeString) var).getValue();
} else {
throw new InvalidInputException(interpreter, this, InvalidReason.STRING);
}

// Minor optimization, avoid checking short strings
if (s.length() > 100) {
checkLength(interpreter, s, this);
}

String regex = args[0];

try {
Pattern p = Pattern.compile(regex);
Matcher matcher = p.matcher(s);

return matcher.find();
} catch (PatternSyntaxException e) {
throw new InvalidArgumentException(
interpreter,
this,
InvalidReason.REGEX,
0,
regex
);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.hubspot.jinjava.lib.exptest;

import static org.assertj.core.api.Assertions.assertThat;

import com.google.common.collect.ImmutableMap;
import com.hubspot.jinjava.BaseJinjavaTest;
import com.hubspot.jinjava.objects.SafeString;
import java.util.Arrays;
import org.junit.Test;

public class IsStringMatchingRegexExpTestTest extends BaseJinjavaTest {

private static final String MATCHING_TEMPLATE =
"{{ var is string_matching_regex arg }}";

@Test
public void itReturnsTrueForMatchingRegex() {
assertThat(
jinjava.render(
MATCHING_TEMPLATE,
ImmutableMap.of("var", "It costs $300", "arg", "[0-9]+")
)
)
.isEqualTo("true");
}

@Test
public void itReturnsFalseForNonMatchingRegex() {
assertThat(
jinjava.render(
MATCHING_TEMPLATE,
ImmutableMap.of("var", "hello world", "arg", "[0-9]+")
)
)
.isEqualTo("false");
}

@Test
public void itReturnsFalseForNull() {
assertThat(jinjava.render(MATCHING_TEMPLATE, ImmutableMap.of("var", "testing")))
.isEqualTo("false");
}

@Test
public void itWorksForSafeString() {
assertThat(
jinjava.render(
MATCHING_TEMPLATE,
ImmutableMap.of("var", "testing", "arg", new SafeString("^test"))
)
)
.isEqualTo("true");
}

@Test
public void itWorksWithSelectattr() {
String template =
"{% for item in items|selectattr('name', 'string_matching_regex', '^foo') %}{{ item.name }},{% endfor %}";
assertThat(
jinjava.render(
template,
ImmutableMap.of(
"items",
Arrays.asList(
ImmutableMap.of("name", "foobar"),
ImmutableMap.of("name", "baz"),
ImmutableMap.of("name", "foobaz")
)
)
)
)
.isEqualTo("foobar,foobaz,");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package com.hubspot.jinjava.lib.filter;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import com.hubspot.jinjava.BaseInterpretingTest;
import com.hubspot.jinjava.Jinjava;
import com.hubspot.jinjava.JinjavaConfig;
import com.hubspot.jinjava.interpret.InvalidArgumentException;
import com.hubspot.jinjava.interpret.InvalidInputException;
import com.hubspot.jinjava.objects.SafeString;
import org.junit.Before;
import org.junit.Test;

public class MatchesRegexFilterTest extends BaseInterpretingTest {

MatchesRegexFilter filter;

@Before
public void setup() {
filter = new MatchesRegexFilter();
}

@Test
public void expects1Arg() {
assertThatThrownBy(() -> filter.filter("foo", interpreter))
.hasMessageContaining("requires 1 argument");
}

@Test
public void expectsNotNullArg() {
assertThatThrownBy(() -> filter.filter("foo", interpreter, new String[] { null }))
.hasMessageContaining("a valid regex");
}

@Test
public void itReturnsFalseOnNullInput() {
assertThat(filter.filter(null, interpreter, "foo")).isEqualTo(false);
}

@Test
public void itMatchesRegex() {
assertThat(filter.filter("It costs $300", interpreter, "[0-9]+")).isEqualTo(true);
}

@Test
public void itDoesNotMatchRegex() {
assertThat(filter.filter("hello world", interpreter, "[0-9]+")).isEqualTo(false);
}

@Test
public void itMatchesAnchoredRegex() {
assertThat(filter.filter("hello", interpreter, "^[a-z]+$")).isEqualTo(true);
}

@Test
public void itDoesNotMatchAnchoredRegex() {
assertThat(filter.filter("hello123", interpreter, "^[a-z]+$")).isEqualTo(false);
}

@Test(expected = InvalidArgumentException.class)
public void itThrowsExceptionOnInvalidRegex() {
filter.filter("It costs $300", interpreter, "[");
}

@Test
public void itMatchesRegexForSafeString() {
assertThat(filter.filter(new SafeString("It costs $300"), interpreter, "[0-9]+"))
.isEqualTo(true);
}

@Test
public void itLimitsLongInput() {
assertThatThrownBy(() ->
filter.filter(
"a".repeat(101),
new Jinjava(JinjavaConfig.newBuilder().withMaxStringLength(10).build())
.newInterpreter(),
"O"
)
)
.isInstanceOf(InvalidInputException.class)
.hasMessageContaining(
"Invalid input for 'matches_regex': input with length '101' exceeds maximum allowed length of '10'"
);
}
}