-
Notifications
You must be signed in to change notification settings - Fork 207
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
FIX: coercion on json pointer values #1178
Changes from 3 commits
2a200fc
7bd9c85
56c265d
6091ea5
61a8715
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,18 +10,33 @@ | |
import org.opensearch.dataprepper.expression.antlr.DataPrepperExpressionParser; | ||
|
||
import javax.inject.Named; | ||
import java.io.Serializable; | ||
import java.util.HashMap; | ||
import java.util.Map; | ||
import java.util.function.Function; | ||
|
||
@Named | ||
class ParseTreeCoercionService { | ||
private static final Map<Class<? extends Serializable>, Function<Object, Object>> literalTypes; | ||
|
||
static { | ||
literalTypes = new HashMap<>(); | ||
literalTypes.put(String.class, Function.identity()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are all of these keys needed? Could we use something like
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that will not work for Long type which we want to throw exception. Also, for other custom types, it will just return literal objects which I am not sure if we want to support. e.g.
|
||
literalTypes.put(Boolean.class, Function.identity()); | ||
literalTypes.put(Integer.class, Function.identity()); | ||
literalTypes.put(Float.class, Function.identity()); | ||
literalTypes.put(Double.class, v -> ((Double) v).floatValue()); | ||
} | ||
|
||
public Object coercePrimaryTerminalNode(final TerminalNode node, final Event event) { | ||
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); | ||
return resolveJsonPointerValue(jsonPointerWithoutQuotes, event); | ||
case DataPrepperExpressionParser.JsonPointer: | ||
return event.get(nodeStringValue, Object.class); | ||
return resolveJsonPointerValue(nodeStringValue, event); | ||
case DataPrepperExpressionParser.String: | ||
return nodeStringValue; | ||
case DataPrepperExpressionParser.Integer: | ||
|
@@ -42,4 +57,15 @@ public <T> T coerce(final Object obj, Class<T> clazz) throws ExpressionCoercionE | |
} | ||
throw new ExpressionCoercionException("Unable to cast " + obj.getClass().getName() + " into " + clazz.getName()); | ||
} | ||
|
||
private Object resolveJsonPointerValue(final String jsonPointer, final Event event) { | ||
final Object value = event.get(jsonPointer, Object.class); | ||
if (value == null) { | ||
return null; | ||
} else if (literalTypes.containsKey(value.getClass())) { | ||
return literalTypes.get(value.getClass()).apply(value); | ||
} else { | ||
throw new ExpressionCoercionException("Unsupported type for value " + value); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,7 +28,6 @@ | |
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; | ||
|
@@ -97,36 +96,43 @@ void testCoerceTerminalNodeBooleanType() { | |
assertThat(result, equalTo(testBoolean)); | ||
} | ||
|
||
@Test | ||
void testCoerceTerminalNodeJsonPointerType() { | ||
@ParameterizedTest | ||
@MethodSource("provideSupportedJsonPointerValues") | ||
void testCoerceTerminalNodeJsonPointerTypeSupportedValues(final Object testValue) { | ||
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))); | ||
final Event testEvent = testValue == null ? createTestEvent(new HashMap<>()) : | ||
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)); | ||
if (testValue instanceof Double) { | ||
assertThat(result, instanceOf(Float.class)); | ||
assertThat(result, equalTo(((Double) testValue).floatValue())); | ||
} else { | ||
assertThat(result, equalTo(testValue)); | ||
} | ||
} | ||
|
||
@Test | ||
void testCoerceTerminalNodeJsonPointerTypeMissingKey() { | ||
final String testMissingKey = "missingKey"; | ||
final String testJsonPointerKey = "/" + testMissingKey; | ||
final Event testEvent = createTestEvent(new HashMap<>()); | ||
@ParameterizedTest | ||
@MethodSource("provideUnSupportedJsonPointerValues") | ||
void testCoerceTerminalNodeJsonPointerTypeUnSupportedValues(final Object testValue) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually the same method is used for both jsonpointer and escape jsonpointer test case in which keys are separate. So providing the value only will probably make it more compact and focused on the difference (testValue). Thought? |
||
final String testKey1 = "key1"; | ||
final String testKey2 = "key2"; | ||
final String testJsonPointerKey = String.format("/%s/%s", testKey1, testKey2); | ||
final Event testEvent = testValue == null ? createTestEvent(new HashMap<>()) : | ||
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, nullValue()); | ||
assertThrows(ExpressionCoercionException.class, () -> objectUnderTest.coercePrimaryTerminalNode(terminalNode, testEvent)); | ||
} | ||
|
||
@ParameterizedTest | ||
@MethodSource("provideKeys") | ||
void testCoerceTerminalNodeEscapeJsonPointerType(final String testKey, final String testEscapeJsonPointer) | ||
void testCoerceTerminalNodeEscapeJsonPointerTypeWithSpecialCharacters(final String testKey, final String testEscapeJsonPointer) | ||
throws ExpressionCoercionException { | ||
final String testValue = "test value"; | ||
final Event testEvent = createTestEvent(Map.of(testKey, testValue)); | ||
|
@@ -138,6 +144,38 @@ void testCoerceTerminalNodeEscapeJsonPointerType(final String testKey, final Str | |
assertThat(result, equalTo(testValue)); | ||
} | ||
|
||
@ParameterizedTest | ||
@MethodSource("provideSupportedJsonPointerValues") | ||
void testCoerceTerminalNodeEscapeJsonPointerTypeSupportedValues(final Object testValue) { | ||
final String testKey = "testKey"; | ||
final String testEscapeJsonPointerKey = String.format("\"/%s\"", testKey); | ||
final Event testEvent = testValue == null ? createTestEvent(new HashMap<>()) : | ||
createTestEvent(Map.of(testKey, testValue)); | ||
when(token.getType()).thenReturn(DataPrepperExpressionParser.EscapedJsonPointer); | ||
when(terminalNode.getSymbol()).thenReturn(token); | ||
when(terminalNode.getText()).thenReturn(testEscapeJsonPointerKey); | ||
final Object result = objectUnderTest.coercePrimaryTerminalNode(terminalNode, testEvent); | ||
if (testValue instanceof Double) { | ||
assertThat(result, instanceOf(Float.class)); | ||
assertThat(result, equalTo(((Double) testValue).floatValue())); | ||
} else { | ||
assertThat(result, equalTo(testValue)); | ||
} | ||
} | ||
|
||
@ParameterizedTest | ||
@MethodSource("provideUnSupportedJsonPointerValues") | ||
void testCoerceTerminalNodeEscapeJsonPointerTypeUnSupportedValues(final Object testValue) { | ||
final String testKey = "testKey"; | ||
final String testEscapeJsonPointerKey = String.format("\"/%s\"", testKey); | ||
final Event testEvent = testValue == null ? createTestEvent(new HashMap<>()) : | ||
createTestEvent(Map.of(testKey, testValue)); | ||
when(token.getType()).thenReturn(DataPrepperExpressionParser.EscapedJsonPointer); | ||
when(terminalNode.getSymbol()).thenReturn(token); | ||
when(terminalNode.getText()).thenReturn(testEscapeJsonPointerKey); | ||
assertThrows(ExpressionCoercionException.class, () -> objectUnderTest.coercePrimaryTerminalNode(terminalNode, testEvent)); | ||
} | ||
|
||
@Test | ||
void testCoerceTerminalNodeUnsupportedType() { | ||
final Event testEvent = createTestEvent(new HashMap<>()); | ||
|
@@ -184,4 +222,19 @@ private static Stream<Arguments> provideKeys() { | |
Arguments.of("test~0key", "\"/test~00key\"") | ||
); | ||
} | ||
|
||
private static Stream<Arguments> provideSupportedJsonPointerValues() { | ||
return Stream.of( | ||
Arguments.of(1000), | ||
Arguments.of(true), | ||
Arguments.of("test value"), | ||
Arguments.of(1.1f), | ||
Arguments.of(1.1), | ||
Arguments.of((Object) null) | ||
); | ||
} | ||
|
||
private static Stream<Arguments> provideUnSupportedJsonPointerValues() { | ||
return Stream.of(Arguments.of(Long.MAX_VALUE)); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could this be moved to a constructor argument and generated in a bean configuration function? Static variable can make classes difficult to unit test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Works for me.