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

ADD: ParseTreeCoercionService and tests #1153

Merged
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
1 change: 1 addition & 0 deletions data-prepper-expression/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ dependencies {
implementation 'org.apache.logging.log4j:log4j-core'
implementation 'org.apache.logging.log4j:log4j-slf4j-impl'
testImplementation 'org.springframework:spring-test:5.3.15'
testImplementation 'com.fasterxml.jackson.core:jackson-databind:2.13.1'
}

generateGrammarSource {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.dataprepper.expression;

/**
* @since 1.3
* Exception thrown by {@link ParseTreeCoercionService} methods to indicate type coercion failure.
*/
public class ExpressionCoercionException extends Exception {
public ExpressionCoercionException(final String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.dataprepper.expression;

import com.amazon.dataprepper.model.event.Event;
import org.antlr.v4.runtime.tree.TerminalNode;
import org.opensearch.dataprepper.expression.antlr.DataPrepperExpressionParser;

import javax.inject.Named;

@Named
class ParseTreeCoercionService {
public Object coercePrimaryTerminalNode(final TerminalNode node, final Event event) throws ExpressionCoercionException {
final int nodeType = node.getSymbol().getType();
final String nodeStringValue = node.getText();
switch (nodeType) {
case DataPrepperExpressionParser.EscapedJsonPointer:
final String jsonPointerWithoutQuotes = nodeStringValue.substring(1, nodeStringValue.length() - 1);
return event.get(jsonPointerWithoutQuotes, Object.class);
case DataPrepperExpressionParser.JsonPointer:
return event.get(nodeStringValue, Object.class);
case DataPrepperExpressionParser.String:
return nodeStringValue;
case DataPrepperExpressionParser.Integer:
return Integer.valueOf(nodeStringValue);
case DataPrepperExpressionParser.Float:
return Float.valueOf(nodeStringValue);
case DataPrepperExpressionParser.Boolean:
return Boolean.valueOf(nodeStringValue);
default:
throw new ExpressionCoercionException("Unsupported terminal node type symbol string: " +
DataPrepperExpressionParser.VOCABULARY.getDisplayName(nodeType));
sbayer55 marked this conversation as resolved.
Show resolved Hide resolved
}
}

public <T> T coerce(final Object obj, Class<T> clazz) throws ExpressionCoercionException {
if (obj.getClass().isAssignableFrom(clazz)) {
return (T) obj;
}
throw new ExpressionCoercionException("Unable to cast " + obj.getClass().getName() + " into " + clazz.getName());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.dataprepper.expression;

import com.amazon.dataprepper.model.event.Event;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.tree.TerminalNode;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.opensearch.dataprepper.expression.antlr.DataPrepperExpressionParser;
import org.opensearch.dataprepper.expression.util.TestObject;

import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.stream.Stream;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class ParseTreeCoercionServiceTest {
private static final ObjectMapper mapper = new ObjectMapper();

@Mock
private TerminalNode terminalNode;

@Mock
private Token token;

private final ParseTreeCoercionService objectUnderTest = new ParseTreeCoercionService();

@Test
void testCoerceTerminalNodeStringType() throws ExpressionCoercionException {
when(token.getType()).thenReturn(DataPrepperExpressionParser.String);
final String testString = "test string";
when(terminalNode.getSymbol()).thenReturn(token);
when(terminalNode.getText()).thenReturn(testString);
final Event testEvent = createTestEvent(new HashMap<>());
final Object result = objectUnderTest.coercePrimaryTerminalNode(terminalNode, testEvent);
assertThat(result, instanceOf(String.class));
assertThat(result, equalTo(testString));
}

@Test
void testCoerceTerminalNodeIntegerType() throws ExpressionCoercionException {
when(token.getType()).thenReturn(DataPrepperExpressionParser.Integer);
final Integer testInteger = new Random().nextInt();
when(terminalNode.getSymbol()).thenReturn(token);
when(terminalNode.getText()).thenReturn(String.valueOf(testInteger));
final Event testEvent = createTestEvent(new HashMap<>());
final Object result = objectUnderTest.coercePrimaryTerminalNode(terminalNode, testEvent);
assertThat(result, instanceOf(Integer.class));
assertThat(result, equalTo(testInteger));
}

@Test
void testCoerceTerminalNodeFloatType() throws ExpressionCoercionException {
when(token.getType()).thenReturn(DataPrepperExpressionParser.Float);
final Float testFloat = new Random().nextFloat();
when(terminalNode.getSymbol()).thenReturn(token);
when(terminalNode.getText()).thenReturn(String.valueOf(testFloat));
final Event testEvent = createTestEvent(new HashMap<>());
final Object result = objectUnderTest.coercePrimaryTerminalNode(terminalNode, testEvent);
assertThat(result, instanceOf(Float.class));
assertThat(result, equalTo(testFloat));
}

@Test
void testCoerceTerminalNodeJsonPointerType() throws ExpressionCoercionException {
final String testKey1 = "key1";
final String testKey2 = "key2";
final String testValue = "value";
final String testJsonPointerKey = String.format("/%s/%s", testKey1, testKey2);
final Event testEvent = createTestEvent(Map.of(testKey1, Map.of(testKey2, testValue)));
when(token.getType()).thenReturn(DataPrepperExpressionParser.JsonPointer);
when(terminalNode.getSymbol()).thenReturn(token);
when(terminalNode.getText()).thenReturn(testJsonPointerKey);
final Object result = objectUnderTest.coercePrimaryTerminalNode(terminalNode, testEvent);
assertThat(result, instanceOf(String.class));
assertThat(result, equalTo(testValue));
}

@Test
void testCoerceTerminalNodeJsonPointerTypeMissingKey() throws ExpressionCoercionException {
final String testMissingKey = "missingKey";
final String testJsonPointerKey = "/" + testMissingKey;
final Event testEvent = createTestEvent(new HashMap<>());
when(token.getType()).thenReturn(DataPrepperExpressionParser.JsonPointer);
when(terminalNode.getSymbol()).thenReturn(token);
when(terminalNode.getText()).thenReturn(testJsonPointerKey);
final Object result = objectUnderTest.coercePrimaryTerminalNode(terminalNode, testEvent);
assertThat(result, nullValue());
}

@ParameterizedTest
@MethodSource("provideKeys")
Copy link
Member

Choose a reason for hiding this comment

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

Thanks for adding this!

void testCoerceTerminalNodeEscapeJsonPointerType(final String testKey, final String testEscapeJsonPointer)
throws ExpressionCoercionException {
final String testValue = "test value";
final Event testEvent = createTestEvent(Map.of(testKey, testValue));
when(token.getType()).thenReturn(DataPrepperExpressionParser.EscapedJsonPointer);
when(terminalNode.getSymbol()).thenReturn(token);
when(terminalNode.getText()).thenReturn(testEscapeJsonPointer);
final Object result = objectUnderTest.coercePrimaryTerminalNode(terminalNode, testEvent);
assertThat(result, instanceOf(String.class));
assertThat(result, equalTo(testValue));
}

@Test
void testCoerceTerminalNodeUnsupportedType() {
final Event testEvent = createTestEvent(new HashMap<>());
when(terminalNode.getSymbol()).thenReturn(token);
when(token.getType()).thenReturn(-1);
assertThrows(ExpressionCoercionException.class, () -> objectUnderTest.coercePrimaryTerminalNode(terminalNode, testEvent));
}

@Test
void testCoerceSuccess() throws ExpressionCoercionException {
final Object testObj = false;
final Boolean result = objectUnderTest.coerce(testObj, Boolean.class);
assertThat(result, instanceOf(Boolean.class));
assertThat(result, is(false));
}

@Test
void testCoerceFailure() {
final Object testObj = new TestObject("");
assertThrows(ExpressionCoercionException.class, () -> objectUnderTest.coerce(testObj, String.class));
}

private Event createTestEvent(final Object data) {
Copy link
Member

Choose a reason for hiding this comment

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

You could decouple your tests from JacksonEvent and probably simplify these tests by using a mock. But, I can accept the current code.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Turns out I do have to mock Event in order to test tricky characters in escape jsonpointer which is not currently supported by JacksonEvent.

final Event event = mock(Event.class);
final JsonNode node = mapper.valueToTree(data);
lenient().when(event.get(anyString(), any())).thenAnswer(invocation -> {
Object[] args = invocation.getArguments();
final String jsonPointer = (String) args[0];
final Class<?> clazz = (Class<?>) args[1];
final JsonNode childNode = node.at(jsonPointer);
if (childNode.isMissingNode()) {
return null;
}
return mapper.treeToValue(childNode, clazz);
});
return event;
}

private static Stream<Arguments> provideKeys() {
return Stream.of(
Arguments.of("test key", "\"/test key\""),
Arguments.of("test/key", "\"/test~1key\""),
Arguments.of("test\\key", "\"/test\\key\""),
Arguments.of("test~0key", "\"/test~00key\"")
);
}
}