From a507bddb15209c64fb360d2fdde490e5d9bc4130 Mon Sep 17 00:00:00 2001 From: Taylor Gray Date: Thu, 15 Aug 2024 16:53:43 -0500 Subject: [PATCH] Add startsWith expression function Signed-off-by: Taylor Gray --- .../StartsWithExpressionFunction.java | 53 +++++++++ .../ContainsExpressionFunctionTest.java | 3 +- ...ericExpressionEvaluator_ConditionalIT.java | 7 +- .../StartsWithExpressionFunctionTest.java | 112 ++++++++++++++++++ 4 files changed, 171 insertions(+), 4 deletions(-) create mode 100644 data-prepper-expression/src/main/java/org/opensearch/dataprepper/expression/StartsWithExpressionFunction.java create mode 100644 data-prepper-expression/src/test/java/org/opensearch/dataprepper/expression/StartsWithExpressionFunctionTest.java diff --git a/data-prepper-expression/src/main/java/org/opensearch/dataprepper/expression/StartsWithExpressionFunction.java b/data-prepper-expression/src/main/java/org/opensearch/dataprepper/expression/StartsWithExpressionFunction.java new file mode 100644 index 0000000000..c8e4b77187 --- /dev/null +++ b/data-prepper-expression/src/main/java/org/opensearch/dataprepper/expression/StartsWithExpressionFunction.java @@ -0,0 +1,53 @@ +package org.opensearch.dataprepper.expression; + +import org.opensearch.dataprepper.model.event.Event; + +import javax.inject.Named; +import java.util.List; +import java.util.function.Function; + +@Named +public class StartsWithExpressionFunction implements ExpressionFunction { + private static final int NUMBER_OF_ARGS = 2; + + static final String STARTS_WITH_FUNCTION_NAME = "startsWith"; + @Override + public String getFunctionName() { + return STARTS_WITH_FUNCTION_NAME; + } + + @Override + public Object evaluate( + final List args, + final Event event, + final Function convertLiteralType) { + + if (args.size() != NUMBER_OF_ARGS) { + throw new RuntimeException("startsWith() takes exactly two arguments"); + } + + String[] strArgs = new String[NUMBER_OF_ARGS]; + for (int i = 0; i < NUMBER_OF_ARGS; i++) { + Object arg = args.get(i); + if (!(arg instanceof String)) { + throw new RuntimeException(String.format("startsWith() takes only string type arguments. \"%s\" is not of type string", arg)); + } + String stringOrKey = (String) arg; + if (stringOrKey.charAt(0) == '"') { + strArgs[i] = stringOrKey.substring(1, stringOrKey.length()-1); + } else if (stringOrKey.charAt(0) == '/') { + Object obj = event.get(stringOrKey, Object.class); + if (obj == null) { + return false; + } + if (!(obj instanceof String)) { + throw new RuntimeException(String.format("startsWith() only operates on string types. The value at \"%s\" is \"%s\" which is not a string type.", stringOrKey, obj)); + } + strArgs[i] = (String)obj; + } else { + throw new RuntimeException(String.format("Arguments to startsWith() must be a literal string or a Json Pointer. \"%s\" is not string literal or json pointer", stringOrKey)); + } + } + return strArgs[0].startsWith(strArgs[1]); + } +} diff --git a/data-prepper-expression/src/test/java/org/opensearch/dataprepper/expression/ContainsExpressionFunctionTest.java b/data-prepper-expression/src/test/java/org/opensearch/dataprepper/expression/ContainsExpressionFunctionTest.java index b24086b231..3497caa10d 100644 --- a/data-prepper-expression/src/test/java/org/opensearch/dataprepper/expression/ContainsExpressionFunctionTest.java +++ b/data-prepper-expression/src/test/java/org/opensearch/dataprepper/expression/ContainsExpressionFunctionTest.java @@ -56,8 +56,7 @@ public ContainsExpressionFunction createObjectUnderTest() { } @Test - void testContainsBasic() { - containsExpressionFunction = createObjectUnderTest(); + void testContainsBasic() {containsExpressionFunction = createObjectUnderTest(); assertThat(containsExpressionFunction.evaluate(List.of("\"abcde\"", "\"abcd\""), testEvent, testFunction), equalTo(true)); assertThat(containsExpressionFunction.evaluate(List.of("/"+testKey, "/"+testKey2), testEvent, testFunction), equalTo(true)); assertThat(containsExpressionFunction.evaluate(List.of("\""+testValue+"\"", "/"+testKey2), testEvent, testFunction), equalTo(true)); diff --git a/data-prepper-expression/src/test/java/org/opensearch/dataprepper/expression/GenericExpressionEvaluator_ConditionalIT.java b/data-prepper-expression/src/test/java/org/opensearch/dataprepper/expression/GenericExpressionEvaluator_ConditionalIT.java index 49ba051d92..d4741c539a 100644 --- a/data-prepper-expression/src/test/java/org/opensearch/dataprepper/expression/GenericExpressionEvaluator_ConditionalIT.java +++ b/data-prepper-expression/src/test/java/org/opensearch/dataprepper/expression/GenericExpressionEvaluator_ConditionalIT.java @@ -21,6 +21,7 @@ import java.util.List; import java.util.Map; import java.util.Random; +import java.util.UUID; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; @@ -234,8 +235,10 @@ private static Stream validExpressionArguments() { arguments("/name =~ \".*dataprepper-[0-9]+\"", event("{\"name\": \"dataprepper-0\"}"), true), arguments("/name =~ \".*dataprepper-[0-9]+\"", event("{\"name\": \"dataprepper-212\"}"), true), arguments("/name =~ \".*dataprepper-[0-9]+\"", event("{\"name\": \"dataprepper-abc\"}"), false), - arguments("/name =~ \".*dataprepper-[0-9]+\"", event("{\"other\": \"dataprepper-abc\"}"), false) - ); + arguments("/name =~ \".*dataprepper-[0-9]+\"", event("{\"other\": \"dataprepper-abc\"}"), false), + arguments("startsWith(\""+strValue+ UUID.randomUUID() + "\",/status)", event("{\"status\":\""+strValue+"\"}"), true), + arguments("startsWith(\""+ UUID.randomUUID() +strValue+ "\",/status)", event("{\"status\":\""+strValue+"\"}"), false) + ); } private static Stream invalidExpressionArguments() { diff --git a/data-prepper-expression/src/test/java/org/opensearch/dataprepper/expression/StartsWithExpressionFunctionTest.java b/data-prepper-expression/src/test/java/org/opensearch/dataprepper/expression/StartsWithExpressionFunctionTest.java new file mode 100644 index 0000000000..b25acc1298 --- /dev/null +++ b/data-prepper-expression/src/test/java/org/opensearch/dataprepper/expression/StartsWithExpressionFunctionTest.java @@ -0,0 +1,112 @@ +package org.opensearch.dataprepper.expression; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.opensearch.dataprepper.model.event.Event; +import org.opensearch.dataprepper.model.event.JacksonEvent; + +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.function.Function; +import java.util.stream.Stream; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.opensearch.dataprepper.expression.StartsWithExpressionFunction.STARTS_WITH_FUNCTION_NAME; + +public class StartsWithExpressionFunctionTest { + + private Event testEvent; + + private Event createTestEvent(final Object data) { + return JacksonEvent.builder().withEventType("event").withData(data).build(); + } + + private ExpressionFunction createObjectUnderTest() { + return new StartsWithExpressionFunction(); + } + + @ParameterizedTest + @MethodSource("validStartsWithProvider") + void startsWith_returns_expected_result_when_evaluated( + final String value, final String prefix, final boolean expectedResult) { + final String key = "test_key"; + testEvent = createTestEvent(Map.of(key, value)); + + final ExpressionFunction objectUnderTest = createObjectUnderTest(); + assertThat(objectUnderTest.getFunctionName(), equalTo(STARTS_WITH_FUNCTION_NAME)); + + final Object result = objectUnderTest.evaluate(List.of("/" + key, "\"" + prefix + "\""), testEvent, mock(Function.class)); + + assertThat(result, equalTo(expectedResult)); + } + + @Test + void startsWith_with_a_key_as_the_prefix_returns_expected_result() { + + final String prefixKey = "prefix"; + final String prefixValue = "te"; + + final String key = "test_key"; + final String value = "test"; + testEvent = createTestEvent(Map.of(key, value, prefixKey, prefixValue)); + + final ExpressionFunction objectUnderTest = createObjectUnderTest(); + assertThat(objectUnderTest.getFunctionName(), equalTo(STARTS_WITH_FUNCTION_NAME)); + + final Object result = objectUnderTest.evaluate(List.of("/" + key, "/" + prefixKey), testEvent, mock(Function.class)); + + assertThat(result, equalTo(true)); + } + + @Test + void startsWith_returns_false_when_key_does_not_exist_in_Event() { + final String key = "test_key"; + testEvent = createTestEvent(Map.of(UUID.randomUUID().toString(), UUID.randomUUID().toString())); + + final ExpressionFunction startsWithExpressionFunction = createObjectUnderTest(); + final Object result = startsWithExpressionFunction.evaluate(List.of("/" + key, "\"abcd\""), testEvent, mock(Function.class)); + + assertThat(result, equalTo(false)); + } + + @Test + void startsWith_without_2_arguments_throws_RuntimeException() { + final ExpressionFunction startsWithExpressionFunction = createObjectUnderTest(); + assertThrows(RuntimeException.class, () -> startsWithExpressionFunction.evaluate(List.of("abcd"), testEvent, mock(Function.class))); + } + + @ParameterizedTest + @MethodSource("invalidStartsWithProvider") + void invalid_startsWith_arguments_throws_RuntimeException(final String firstArg, final Object secondArg, final Object value) { + final ExpressionFunction startsWithExpressionFunction = createObjectUnderTest(); + final String testKey = "test_key"; + + assertThrows(RuntimeException.class, () -> startsWithExpressionFunction.evaluate(List.of(firstArg, secondArg), createTestEvent(Map.of(testKey, value)), mock(Function.class))); + } + + private static Stream validStartsWithProvider() { + return Stream.of( + Arguments.of("{test", "{te", true), + Arguments.of("{test", "{", true), + Arguments.of("test", "{", false), + Arguments.of("MyPrefix", "My", true), + Arguments.of("MyPrefix", "Prefix", false) + ); + } + + private static Stream invalidStartsWithProvider() { + return Stream.of( + Arguments.of("\"abc\"", "/test_key", 1234), + Arguments.of("abcd", "/test_key", "value"), + Arguments.of("\"abcd\"", "/test_key", 1234), + Arguments.of("\"/test_key\"", 1234, "value") + ); + } +}