-
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 1 commit
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,28 @@ | |
import org.opensearch.dataprepper.expression.antlr.DataPrepperExpressionParser; | ||
|
||
import javax.inject.Named; | ||
import java.io.Serializable; | ||
import java.util.Arrays; | ||
import java.util.List; | ||
|
||
@Named | ||
class ParseTreeCoercionService { | ||
private final List<? extends Class<? extends Serializable>> literalTypes = Arrays.asList( | ||
String.class, | ||
Boolean.class, | ||
Integer.class, | ||
Float.class | ||
); | ||
|
||
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 +52,17 @@ 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) { | ||
throw new ExpressionCoercionException("Unsupported type for value " + value); | ||
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. Why does a null value throw? I see that there is no clear default value to use in this case. Maybe just return a new 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. Right now there is no null value support in the grammar literal types so it is impossible for cx to check jsonpointer value is null in conditional expression. We have two options for null appearing in jsonPointer value:
I think both should work and I do not favor one over the other. @sbayer55 thoughts? 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. So in the current design 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. Looking at this code (currently in PR), I think the equals operator would work correctly if the We could even do something like this:
And return:
|
||
} else if (literalTypes.contains(value.getClass())) { | ||
return value; | ||
} else if (value instanceof Double) { | ||
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. This approach would be easier to extend if we have other possible types.
And here:
But, this is all internal and later refactoring is light. So I don't have a strong opinion on it. |
||
return ((Double) value).floatValue(); | ||
} 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,42 @@ 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))); | ||
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 +143,37 @@ 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 = 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 +220,21 @@ 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) | ||
); | ||
} | ||
|
||
private static Stream<Arguments> provideUnSupportedJsonPointerValues() { | ||
return Stream.of( | ||
Arguments.of(Long.MAX_VALUE), | ||
Arguments.of((Object) null) | ||
); | ||
} | ||
} |
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.
This can be static.