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 'typeof' operator to DataPrepper expressions #4500

Merged
merged 7 commits into from
May 6, 2024
Merged
Show file tree
Hide file tree
Changes from 6 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 @@ -46,7 +46,21 @@ public enum DataType {
*
* @since 2.8
*/
DOUBLE("double");
DOUBLE("double"),

/**
* Type of <i>map</i>. Compatible with the Java <b>map</b> primitive data type.
*
* @since 2.8
*/
MAP("map"),

/**
* Type of <i>array</i>. Compatible with the Java <b>array</b> primitive data type.
*
* @since 2.8
*/
ARRAY("array");

private static final Map<String, DataType> TYPES_MAP = Arrays.stream(DataType.values())
.collect(Collectors.toMap(
Expand Down Expand Up @@ -75,4 +89,26 @@ public String getTypeName() {
static DataType fromTypeName(final String option) {
return TYPES_MAP.get(option);
}

public static boolean isSameType(final Object object, final String option) {
DataType type = fromTypeName(option);
if (type == null)
dinujoh marked this conversation as resolved.
Show resolved Hide resolved
throw new IllegalArgumentException("Unknown DataType");
switch (type) {
Copy link
Member

@dinujoh dinujoh May 6, 2024

Choose a reason for hiding this comment

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

we should add a case condition for Float & BigDecimal.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

After discussion, decided not to support these two for now as double handles float and all BigDecimals are actually strings.

case MAP:
return (object instanceof Map);
case ARRAY:
return (object.getClass().isArray());
case DOUBLE:
return (object instanceof Double);
case BOOLEAN:
return (object instanceof Boolean);
case INTEGER:
return (object instanceof Integer);
case LONG:
return (object instanceof Long);
default: // STRING
return (object instanceof String);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,46 @@

package org.opensearch.dataprepper.model.event;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;

import java.util.Map;

class DataTypeTest {
@ParameterizedTest
@EnumSource(DataType.class)
void fromTypeName_returns_expected_value(final DataType dataType) {
assertThat(DataType.fromTypeName(dataType.getTypeName()), equalTo(dataType));
}
}

@Test
void test_isSameType_withInvalidInput() {
assertThrows(IllegalArgumentException.class, () -> DataType.isSameType(2, "unknown"));
}

@Test
Copy link
Member

Choose a reason for hiding this comment

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

Consider adding parameterized test to cover all cases.

void test_isSameType() {
int testArray[] = {1,2};
String testNotArray = "testString";
assertThat(DataType.isSameType(2, "integer"), equalTo(true));
assertThat(DataType.isSameType("testString", "string"), equalTo(true));
assertThat(DataType.isSameType(2L, "long"), equalTo(true));
assertThat(DataType.isSameType(2.0, "double"), equalTo(true));
assertThat(DataType.isSameType(true, "boolean"), equalTo(true));
assertThat(DataType.isSameType(Map.of("k", "v"), "map"), equalTo(true));
assertThat(DataType.isSameType(testArray, "array"), equalTo(true));

assertThat(DataType.isSameType(false, "integer"), equalTo(false));
assertThat(DataType.isSameType(2, "string"), equalTo(false));
assertThat(DataType.isSameType("testString", "long"), equalTo(false));
assertThat(DataType.isSameType("testString", "double"), equalTo(false));
assertThat(DataType.isSameType(2, "boolean"), equalTo(false));
assertThat(DataType.isSameType("testString", "map"), equalTo(false));
assertThat(DataType.isSameType(testNotArray, "array"), equalTo(false));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,9 @@ regexEqualityOperator

relationalOperatorExpression
: relationalOperatorExpression relationalOperator setOperatorExpression
| relationalOperatorExpression relationalOperator typeOfOperatorExpression
| setOperatorExpression
| typeOfOperatorExpression
;

relationalOperator
Expand All @@ -88,6 +90,10 @@ relationalOperator
| GTE
;

typeOfOperatorExpression
: JsonPointer TYPEOF String
;

setOperatorExpression
: setOperatorExpression setOperator setInitializer
| unaryOperatorExpression
Expand Down Expand Up @@ -298,6 +304,7 @@ MATCH_REGEX_PATTERN : '=~';
NOT_MATCH_REGEX_PATTERN : '!~';
IN_SET : SPACE 'in' SPACE;
NOT_IN_SET : SPACE 'not in' SPACE;
TYPEOF: SPACE 'typeof' SPACE;
AND : SPACE 'and' SPACE;
OR : SPACE 'or' SPACE;
NOT : 'not' SPACE;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.dataprepper.expression;

import org.antlr.v4.runtime.RuleContext;
import org.opensearch.dataprepper.expression.antlr.DataPrepperExpressionParser;

import java.util.function.BiPredicate;

import static com.google.common.base.Preconditions.checkArgument;

class GenericTypeOfOperator implements Operator<Boolean> {
private final int symbol;
private final String displayName;
private final BiPredicate<Object, Object> operation;

public GenericTypeOfOperator(final int symbol, BiPredicate<Object, Object> operation) {
this.symbol = symbol;
displayName = DataPrepperExpressionParser.VOCABULARY.getDisplayName(symbol);
this.operation = operation;
}

@Override
public boolean shouldEvaluate(final RuleContext ctx) {
return ctx.getRuleIndex() == DataPrepperExpressionParser.RULE_typeOfOperatorExpression;
}

@Override
public int getSymbol() {
return symbol;
}

@Override
public Boolean evaluate(final Object ... args) {
checkArgument(args.length == 2, displayName + " requires operands length to be 2.");
return operation.test(args[0], args[1]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
package org.opensearch.dataprepper.expression;

import org.opensearch.dataprepper.expression.antlr.DataPrepperExpressionParser;
import org.opensearch.dataprepper.model.event.DataType;
import org.springframework.context.annotation.Bean;

import javax.inject.Named;
Expand All @@ -22,6 +23,7 @@ class OperatorConfiguration {
public final BiPredicate<Object, Object> regexEquals = (x, y) -> ((String) x).matches((String) y);
public final BiPredicate<Object, Object> equals = Objects::equals;
public final BiPredicate<Object, Object> inSet = (x, y) -> ((Set<?>) y).contains(x);
public final BiPredicate<Object, Object> typeOf = (x, y) -> DataType.isSameType(x, (String)y);

@Bean
public NumericCompareOperator greaterThanOperator() {
Expand Down Expand Up @@ -276,6 +278,11 @@ public AddBinaryOperator concatOperator() {
return new AddBinaryOperator(DataPrepperExpressionParser.PLUS, null);
}

@Bean
public GenericTypeOfOperator typeOfOperator() {
return new GenericTypeOfOperator(DataPrepperExpressionParser.TYPEOF, typeOf);
}

@Bean
public AddBinaryOperator addOperator() {
final Map<Class<? extends Number>, Map<Class<? extends Number>, BiFunction<Object, Object, Number>>>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.dataprepper.expression;

import org.antlr.v4.runtime.ParserRuleContext;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.opensearch.dataprepper.expression.antlr.DataPrepperExpressionParser;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.when;

import java.util.Map;

@ExtendWith(MockitoExtension.class)
class TypeOfOperatorTest {
final GenericTypeOfOperator objectUnderTest = new OperatorConfiguration().typeOfOperator();

@Mock
private ParserRuleContext ctx;

@Test
void testGetNumberOfOperands() {
assertThat(objectUnderTest.getNumberOfOperands(ctx), is(2));
}

@Test
void testShouldEvaluate() {
when(ctx.getRuleIndex()).thenReturn(DataPrepperExpressionParser.RULE_typeOfOperatorExpression);
assertThat(objectUnderTest.shouldEvaluate(ctx), is(true));
when(ctx.getRuleIndex()).thenReturn(-1);
assertThat(objectUnderTest.shouldEvaluate(ctx), is(false));
}

@Test
void testGetSymbol() {
assertThat(objectUnderTest.getSymbol(), is(DataPrepperExpressionParser.TYPEOF));
}

@Test
void testEvalValidArgs() {
Copy link
Member

@dinujoh dinujoh May 6, 2024

Choose a reason for hiding this comment

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

this can be parameterized test as well.

assertThat(objectUnderTest.evaluate(2, "integer"), is(true));
assertThat(objectUnderTest.evaluate(2, "string"), is(false));
assertThat(objectUnderTest.evaluate("testString", "string"), is(true));
assertThat(objectUnderTest.evaluate("testString", "double"), is(false));
assertThat(objectUnderTest.evaluate(true, "boolean"), is(true));
assertThat(objectUnderTest.evaluate(1, "boolean"), is(false));
assertThat(objectUnderTest.evaluate(1L, "long"), is(true));
assertThat(objectUnderTest.evaluate(1.0, "long"), is(false));
assertThat(objectUnderTest.evaluate(1.0, "double"), is(true));
assertThat(objectUnderTest.evaluate(1L, "double"), is(false));
assertThat(objectUnderTest.evaluate(Map.of("k", "v"), "map"), is(true));
assertThat(objectUnderTest.evaluate(1L, "map"), is(false));
int testArray[] = {1,2};
assertThat(objectUnderTest.evaluate(testArray, "array"), is(true));
assertThat(objectUnderTest.evaluate(1L, "array"), is(false));
}

@Test
void testEvalInValidArgLength() {
assertThrows(IllegalArgumentException.class, () -> objectUnderTest.evaluate(1, "integer", 2));
}

@Test
void testEvalInValidArgType() {
assertThrows(IllegalArgumentException.class, () -> objectUnderTest.evaluate(1, "unknown"));
}
}

Loading