Skip to content

Add support for parameterized and generic types in DataConverter (#2413) #2605

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

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
import io.temporal.failure.DefaultFailureConverter;
import io.temporal.payload.codec.PayloadCodec;
import io.temporal.payload.context.SerializationContext;
import java.lang.reflect.Array;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.Optional;
Expand Down Expand Up @@ -132,7 +135,7 @@ default Object[] fromPayloads(
if (!content.isPresent()) {
// Return defaults for all the parameters
for (int i = 0; i < parameterTypes.length; i++) {
result[i] = Defaults.defaultValue((Class<?>) genericParameterTypes[i]);
result[i] = Defaults.defaultValue(getRawClass(genericParameterTypes[i]));
}
return result;
}
Expand All @@ -142,7 +145,7 @@ default Object[] fromPayloads(
Class<?> pt = parameterTypes[i];
Type gt = genericParameterTypes[i];
if (i >= count) {
result[i] = Defaults.defaultValue((Class<?>) gt);
result[i] = Defaults.defaultValue(getRawClass(gt));
} else {
result[i] = this.fromPayload(payloads.getPayloads(i), pt, gt);
}
Expand Down Expand Up @@ -214,4 +217,25 @@ static Object[] arrayFromPayloads(
throws DataConverterException {
return converter.fromPayloads(content, parameterTypes, genericParameterTypes);
}

/**
* Extract the raw Class from a Type; handles regular classes, parameterized types, and generic
* array types.
*
* @param type the Type to extract from (could be Class, ParameterizedType, or GenericArrayType)
* @return the raw Class for the type
*/
static Class<?> getRawClass(Type type) {
if (type instanceof Class) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need to handle any other type? I might just need to test but isn't GenericArrayType possible as well?

Copy link
Author

Choose a reason for hiding this comment

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

Good catch regarding GenericArrayType - I've added support for that as well now.

return (Class<?>) type;
} else if (type instanceof ParameterizedType) {
return (Class<?>) ((ParameterizedType) type).getRawType();
} else if (type instanceof GenericArrayType) {
Type componentType = ((GenericArrayType) type).getGenericComponentType();
Class<?> componentClass = getRawClass(componentType);
return Array.newInstance(componentClass, 0).getClass();
} else {
return Object.class;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package io.temporal.common.converter;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import io.temporal.api.common.v1.Payloads;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Optional;
import org.junit.Test;

public class DataConverterGenericTypeTest {

@Test
public void testFromPayloadsWithGenericListParameter() throws Exception {
DataConverter dataConverter = DefaultDataConverter.newDefaultInstance();
Method method = TestInterface.class.getMethod("processData", String.class, List.class);
Optional<Payloads> payloads = dataConverter.toPayloads("test-data");

Object[] args =
dataConverter.fromPayloads(
payloads, method.getParameterTypes(), method.getGenericParameterTypes());

assertEquals(2, args.length);
assertEquals("test-data", args[0]);
assertNull(args[1]);
}

@Test
public void testFromPayloadsWithEmptyPayloads() throws Exception {
DataConverter dataConverter = DefaultDataConverter.newDefaultInstance();
Method method = TestInterface.class.getMethod("processData", String.class, List.class);
Object[] args =
dataConverter.fromPayloads(
Optional.empty(), method.getParameterTypes(), method.getGenericParameterTypes());

assertEquals(2, args.length);
assertNull(args[0]);
assertNull(args[1]);
}

@Test
public void testGetRawClassWithGenericArrayType() throws Exception {
Method method = TestInterface.class.getMethod("processGenericArray", Object[].class);
Type[] genericParameterTypes = method.getGenericParameterTypes();
Type genericArrayType = genericParameterTypes[0];

assertTrue("Expected GenericArrayType instance", genericArrayType instanceof GenericArrayType);

Class<?> rawClass = DataConverter.getRawClass(genericArrayType);
assertEquals(Object[].class, rawClass);
}

@Test
public void testGetRawClassWithParameterizedType() throws Exception {
Method method = TestInterface.class.getMethod("processData", String.class, List.class);
Type[] genericParameterTypes = method.getGenericParameterTypes();
Type parameterizedType = genericParameterTypes[1];

assertTrue(
"Expected ParameterizedType instance", parameterizedType instanceof ParameterizedType);

Class<?> rawClass = DataConverter.getRawClass(parameterizedType);
assertEquals(List.class, rawClass);
}

@Test
public void testGetRawClassWithRegularClass() {
Class<?> rawClass = DataConverter.getRawClass(String.class);
assertEquals(String.class, rawClass);
}

@Test
public void testGetRawClassWithUnsupportedType() {
Type unsupportedType = new Type() {};
Class<?> rawClass = DataConverter.getRawClass(unsupportedType);
assertEquals(Object.class, rawClass);
}

public interface TestInterface {
void processData(String data, List<String> items);

<T> void processGenericArray(T[] items);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import io.temporal.activity.ActivityInterface;
import io.temporal.client.WorkflowClient;
import io.temporal.client.WorkflowStub;
import io.temporal.testing.internal.SDKTestOptions;
import io.temporal.testing.internal.SDKTestWorkflowRule;
import java.time.Duration;
Expand All @@ -19,7 +20,8 @@ public class GenericParametersWorkflowTest {
@Rule
public SDKTestWorkflowRule testWorkflowRule =
SDKTestWorkflowRule.newBuilder()
.setWorkflowTypes(GenericParametersWorkflowImpl.class)
.setWorkflowTypes(
GenericParametersWorkflowImpl.class, MissingParametersWorkflowImpl.class)
.setActivityImplementations(activitiesImpl)
.build();

Expand Down Expand Up @@ -61,6 +63,14 @@ public void testGenericParametersWorkflow() throws ExecutionException, Interrupt
Assert.assertEquals(expectedResult, result);
}

@Test
public void testMissingGenericParameterWithUntypedStub() {
WorkflowStub untypedStub = testWorkflowRule.newUntypedWorkflowStub("MissingParametersWorkflow");
untypedStub.start("test-name");
String result = untypedStub.getResult(String.class);
Assert.assertEquals("test-name", result);
}

@ActivityInterface
public interface GenericParametersActivity {

Expand All @@ -80,6 +90,13 @@ public interface GenericParametersWorkflow {
List<UUID> query(List<UUID> arg);
}

@WorkflowInterface
public interface MissingParametersWorkflow {

@WorkflowMethod
String execute(String name, List<String> names);
}

public static class GenericParametersActivityImpl implements GenericParametersActivity {

@Override
Expand Down Expand Up @@ -119,4 +136,16 @@ public List<UUID> query(List<UUID> arg) {
return result;
}
}

public static class MissingParametersWorkflowImpl implements MissingParametersWorkflow {

@Override
public String execute(String name, List<String> names) {
if (names == null) {
return name;
} else {
return name + ":" + String.join(",", names);
}
}
}
}