Skip to content

Commit ef198a2

Browse files
committed
feat(sdk): support indirect DurableHandler inheritance
1 parent d8fd15d commit ef198a2

3 files changed

Lines changed: 73 additions & 12 deletions

File tree

sdk/src/main/java/software/amazon/lambda/durable/DurableHandler.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public abstract class DurableHandler<I, O> implements RequestStreamHandler {
3131
private static final Logger logger = LoggerFactory.getLogger(DurableHandler.class);
3232

3333
protected DurableHandler() {
34-
this.inputType = TypeToken.fromGenericSuperClass(getClass(), 0);
34+
this.inputType = TypeToken.fromGenericSuperClass(getClass(), DurableHandler.class, 0);
3535
this.config = createConfiguration();
3636
}
3737

sdk/src/main/java/software/amazon/lambda/durable/TypeToken.java

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import java.lang.reflect.ParameterizedType;
66
import java.lang.reflect.Type;
7+
import java.lang.reflect.TypeVariable;
78

89
/**
910
* Framework-agnostic type token for capturing generic type information at runtime.
@@ -60,22 +61,40 @@ public static <U> TypeToken<U> get(Class<U> clazz) {
6061
}
6162

6263
/**
63-
* Creates a TypeToken by extracting a type parameter from a generic superclass.
64+
* Creates a TypeToken by extracting a type parameter from a target ancestor class, walking up the class hierarchy.
6465
*
65-
* @param clazz the subclass to extract the type from
66-
* @param typeParameterPosition the position of the type parameter in the superclass declaration (0-based)
66+
* @param clazz the subclass to start from
67+
* @param targetClass the ancestor class to extract the type parameter from
68+
* @param typeParameterPosition the position of the type parameter in the target class declaration (0-based)
6769
* @param <U> the type to extract
68-
* @param <V> the superclass type
70+
* @param <V> the starting class type
6971
* @return a TypeToken representing the extracted type
7072
*/
71-
static <U, V> TypeToken<U> fromGenericSuperClass(Class<V> clazz, int typeParameterPosition) {
72-
// Extract input type from generic superclass
73-
var superClass = clazz.getGenericSuperclass();
74-
if (superClass instanceof ParameterizedType paramType) {
75-
return new TypeToken<>(paramType.getActualTypeArguments()[typeParameterPosition]) {};
76-
} else {
77-
throw new IllegalArgumentException("Cannot determine type from base class: " + clazz);
73+
static <U, V> TypeToken<U> fromGenericSuperClass(Class<V> clazz, Class<?> targetClass, int typeParameterPosition) {
74+
Class<?> current = clazz;
75+
76+
while (current != null && current != Object.class) {
77+
var genericSuper = current.getGenericSuperclass();
78+
if (!(genericSuper instanceof ParameterizedType paramType)) {
79+
current = current.getSuperclass();
80+
continue;
81+
}
82+
83+
Class<?> rawSuper = (Class<?>) paramType.getRawType();
84+
if (rawSuper == targetClass) {
85+
Type typeArgument = paramType.getActualTypeArguments()[typeParameterPosition];
86+
if (typeArgument instanceof TypeVariable) {
87+
throw new IllegalArgumentException(
88+
"Cannot determine type from base class, the type is left unresolved by an intermediate class: "
89+
+ clazz);
90+
}
91+
return new TypeToken<>(typeArgument) {};
92+
}
93+
94+
current = rawSuper;
7895
}
96+
97+
throw new IllegalArgumentException("Cannot determine type from base class: " + clazz);
7998
}
8099

81100
/**

sdk/src/test/java/software/amazon/lambda/durable/DurableHandlerTest.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,53 @@ void testNonDurableFunctionThrowsUserFriendlyError() throws Exception {
6565
assertTrue(exception.getMessage().contains("Unexpected payload provided to start the durable execution"));
6666
}
6767

68+
@Test
69+
void testIndirectDurableHandlerInheritance() {
70+
// Verifies a handler that reaches DurableHandler through an intermediate
71+
// abstract class still resolves its input type correctly.
72+
var handler = new ConcreteIndirectHandler();
73+
74+
assertNotNull(handler);
75+
76+
var result = handler.handleRequest("test-input", null);
77+
assertEquals("indirect: test-input", result);
78+
}
79+
80+
@Test
81+
void testUnresolvedInputTypeThrowsException() {
82+
// The input type is left as a type variable by the intermediate class, so it
83+
// cannot be resolved and must fail loudly rather than producing a broken token.
84+
var exception = assertThrows(IllegalArgumentException.class, UnresolvedInputHandler::new);
85+
assertTrue(exception.getMessage().contains("unresolved"));
86+
}
87+
6888
// Test handler implementation
6989
private static class TestDurableHandler extends DurableHandler<String, String> {
7090
@Override
7191
public String handleRequest(String input, DurableContext context) {
7292
return "processed: " + input;
7393
}
7494
}
95+
96+
// Intermediate abstract handler that fixes the input type
97+
private abstract static class AbstractIndirectHandler<O> extends DurableHandler<String, O> {}
98+
99+
// Concrete handler that inherits DurableHandler indirectly
100+
private static class ConcreteIndirectHandler extends AbstractIndirectHandler<String> {
101+
@Override
102+
public String handleRequest(String input, DurableContext context) {
103+
return "indirect: " + input;
104+
}
105+
}
106+
107+
// Intermediate handler that leaves the input type as a type variable
108+
private abstract static class UnresolvedInputBase<I> extends DurableHandler<I, String> {}
109+
110+
// Concrete handler whose input type is not resolvable by walking superclasses
111+
private static class UnresolvedInputHandler extends UnresolvedInputBase<String> {
112+
@Override
113+
public String handleRequest(String input, DurableContext context) {
114+
return input;
115+
}
116+
}
75117
}

0 commit comments

Comments
 (0)