Skip to content

Commit 8c74d25

Browse files
committed
Merge branch 'pagejep/generic-serdes' into 'main'
feat(serdes): Add support for Generic types to SerDes interface and default Jackson implementation. See merge request mxschell/aws-durable-execution-sdk-java!12
2 parents 3be5d6e + 0511c09 commit 8c74d25

34 files changed

Lines changed: 1344 additions & 253 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,5 @@ Thumbs.db
3838

3939
# SAM
4040
.aws-sam/
41+
samconfig.toml
4142
samconfig.toml.bak

examples/README.md

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ This module contains example applications demonstrating how to use the AWS Lambd
44

55
# Running Examples with JUnit Tests
66

7-
The examples use the SDK's `LocalDurableTestRunner` to run without deploying to AWS Lambda:
7+
The examples demonstrate both local and cloud testing approaches using the SDK's testing utilities:
8+
9+
## Local Testing (Fast, No AWS Required)
10+
11+
Use `LocalDurableTestRunner` for fast, in-memory testing without deploying to AWS Lambda:
812

913
```bash
1014
# Run all example tests
@@ -14,6 +18,23 @@ mvn test
1418
mvn test -Dtest=SimpleStepExampleTest
1519
```
1620

21+
The local runner skips actual wait times and runs entirely in-memory, making tests fast and suitable for CI/CD pipelines.
22+
23+
## Cloud Testing (End-to-End)
24+
25+
Use `CloudDurableTestRunner` for end-to-end testing against deployed Lambda functions:
26+
27+
```java
28+
var runner = CloudDurableTestRunner.create(
29+
"arn:aws:lambda:us-east-1:123456789012:function:MyFunction",
30+
MyInput.class,
31+
MyOutput.class);
32+
33+
var result = runner.run(new MyInput("test-data"));
34+
```
35+
36+
This approach tests the actual deployed function, including all AWS integrations and durable execution behavior.
37+
1738
# Running Examples with AWS SAM locally
1839

1940
**Build the project:**
@@ -63,4 +84,4 @@ Create `event.json`:
6384
{
6485
"name": "Alice"
6586
}
66-
```
87+
```

examples/samconfig.toml

Lines changed: 0 additions & 8 deletions
This file was deleted.
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
package com.amazonaws.lambda.durable.examples;
4+
5+
import com.amazonaws.lambda.durable.DurableContext;
6+
import com.amazonaws.lambda.durable.DurableHandler;
7+
import com.amazonaws.lambda.durable.StepConfig;
8+
import com.amazonaws.lambda.durable.TypeToken;
9+
import com.amazonaws.lambda.durable.retry.RetryStrategies;
10+
import java.util.HashMap;
11+
import java.util.List;
12+
import java.util.Map;
13+
import org.slf4j.Logger;
14+
import org.slf4j.LoggerFactory;
15+
16+
/**
17+
* Example demonstrating TypeToken support for complex generic types.
18+
*
19+
* <p>This example shows how to use TypeToken to work with generic types like List<String>, Map<String, Object>, and
20+
* nested generics that cannot be represented by simple Class objects.
21+
*/
22+
public class GenericTypesExample extends DurableHandler<GenericTypesExample.Input, GenericTypesExample.Output> {
23+
24+
private static final Logger logger = LoggerFactory.getLogger(GenericTypesExample.class);
25+
26+
public static class Input {
27+
public String userId;
28+
29+
public Input() {}
30+
31+
public Input(String userId) {
32+
this.userId = userId;
33+
}
34+
}
35+
36+
public static class Output {
37+
public List<String> items;
38+
public Map<String, Integer> counts;
39+
public Map<String, List<String>> categories;
40+
41+
public Output() {}
42+
43+
public Output(List<String> items, Map<String, Integer> counts, Map<String, List<String>> categories) {
44+
this.items = items;
45+
this.counts = counts;
46+
this.categories = categories;
47+
}
48+
}
49+
50+
@Override
51+
protected Output handleRequest(Input input, DurableContext context) {
52+
logger.info("Starting generic types example for user: {}", input.userId);
53+
54+
// Step 1: Fetch a list of items (List<String>)
55+
List<String> items = context.step("fetch-items", new TypeToken<List<String>>() {}, () -> {
56+
logger.info("Fetching items for user: {}", input.userId);
57+
return List.of("item1", "item2", "item3", "item4");
58+
});
59+
logger.info("Fetched {} items", items.size());
60+
61+
// Step 2: Count items by category (Map<String, Integer>)
62+
Map<String, Integer> counts =
63+
context.step("count-by-category", new TypeToken<Map<String, Integer>>() {}, () -> {
64+
logger.info("Counting items by category");
65+
var result = new HashMap<String, Integer>();
66+
result.put("electronics", 2);
67+
result.put("books", 1);
68+
result.put("clothing", 1);
69+
return result;
70+
});
71+
logger.info("Counted {} categories", counts.size());
72+
73+
// Step 3: Fetch nested generic type with retry (Map<String, List<String>>)
74+
Map<String, List<String>> categories = context.step(
75+
"fetch-categories",
76+
new TypeToken<Map<String, List<String>>>() {},
77+
() -> {
78+
logger.info("Fetching category details");
79+
var result = new HashMap<String, List<String>>();
80+
result.put("electronics", List.of("laptop", "phone"));
81+
result.put("books", List.of("fiction"));
82+
result.put("clothing", List.of("shirt"));
83+
return result;
84+
},
85+
StepConfig.builder()
86+
.retryStrategy(RetryStrategies.Presets.DEFAULT)
87+
.build());
88+
logger.info("Fetched {} category details", categories.size());
89+
90+
logger.info("Generic types example completed successfully");
91+
return new Output(items, counts, categories);
92+
}
93+
}

examples/src/test/java/com/amazonaws/lambda/durable/examples/CloudBasedIntegrationTest.java

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
13
package com.amazonaws.lambda.durable.examples;
24

5+
import static org.junit.jupiter.api.Assertions.*;
6+
37
import com.amazonaws.lambda.durable.model.ExecutionStatus;
48
import com.amazonaws.lambda.durable.testing.CloudDurableTestRunner;
9+
import java.util.Map;
510
import org.junit.jupiter.api.BeforeAll;
611
import org.junit.jupiter.api.Test;
712
import org.junit.jupiter.api.condition.EnabledIf;
813
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
914
import software.amazon.awssdk.services.sts.StsClient;
1015

11-
import java.util.Map;
12-
13-
import static org.junit.jupiter.api.Assertions.*;
14-
1516
@EnabledIf("isEnabled")
1617
public class CloudBasedIntegrationTest {
1718

@@ -21,7 +22,7 @@ public class CloudBasedIntegrationTest {
2122
static boolean isEnabled() {
2223
var enabled = "true".equals(System.getProperty("test.cloud.enabled"));
2324
if (!enabled) {
24-
System.out.println("⚠️ Cloud integration tests disabled. Enable with -Dtest.cloud.enabled=true");
25+
System.out.println("⚠️ Cloud integration tests disabled. Enable with -Dtest.cloud.enabled=true");
2526
}
2627
return enabled;
2728
}
@@ -40,10 +41,11 @@ static void setup() {
4041
if (account == null || region == null) {
4142
var sts = StsClient.create();
4243
if (account == null) account = sts.getCallerIdentity().account();
43-
if (region == null) region = sts.serviceClientConfiguration().region().id();
44+
if (region == null)
45+
region = sts.serviceClientConfiguration().region().id();
4446
}
4547

46-
System.out.println("☁️ Running cloud integration tests against account " + account + " in " + region);
48+
System.out.println("☁️ Running cloud integration tests against account " + account + " in " + region);
4749
}
4850

4951
private static String arn(String functionName) {
@@ -141,7 +143,8 @@ void testWaitAtLeastExample() {
141143

142144
@Test
143145
void testWaitAtLeastInProcessExample() {
144-
var runner = CloudDurableTestRunner.create(arn("wait-at-least-in-process-example"), GreetingRequest.class, String.class);
146+
var runner = CloudDurableTestRunner.create(
147+
arn("wait-at-least-in-process-example"), GreetingRequest.class, String.class);
145148
var result = runner.run(new GreetingRequest("TestUser"));
146149

147150
assertEquals(ExecutionStatus.SUCCEEDED, result.getStatus());
@@ -154,4 +157,41 @@ void testWaitAtLeastInProcessExample() {
154157
assertNotNull(asyncOp);
155158
assertTrue(asyncOp.getStepResult(String.class).contains("Processed: TestUser"));
156159
}
160+
161+
@Test
162+
void testGenericTypesExample() {
163+
var runner = CloudDurableTestRunner.create(
164+
arn("generic-types-example"), GenericTypesExample.Input.class, GenericTypesExample.Output.class);
165+
var result = runner.run(new GenericTypesExample.Input("user123"));
166+
167+
assertEquals(ExecutionStatus.SUCCEEDED, result.getStatus());
168+
169+
GenericTypesExample.Output output = result.getResult(GenericTypesExample.Output.class);
170+
assertNotNull(output);
171+
172+
// Verify items list
173+
assertNotNull(output.items);
174+
assertEquals(4, output.items.size());
175+
assertTrue(output.items.contains("item1"));
176+
assertTrue(output.items.contains("item4"));
177+
178+
// Verify counts map
179+
assertNotNull(output.counts);
180+
assertEquals(3, output.counts.size());
181+
assertEquals(2, output.counts.get("electronics"));
182+
assertEquals(1, output.counts.get("books"));
183+
assertEquals(1, output.counts.get("clothing"));
184+
185+
// Verify categories nested map
186+
assertNotNull(output.categories);
187+
assertEquals(3, output.categories.size());
188+
assertEquals(2, output.categories.get("electronics").size());
189+
assertTrue(output.categories.get("electronics").contains("laptop"));
190+
assertTrue(output.categories.get("electronics").contains("phone"));
191+
192+
// Verify operations were executed
193+
assertNotNull(runner.getOperation("fetch-items"));
194+
assertNotNull(runner.getOperation("count-by-category"));
195+
assertNotNull(runner.getOperation("fetch-categories"));
196+
}
157197
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
package com.amazonaws.lambda.durable.examples;
4+
5+
import static org.junit.jupiter.api.Assertions.*;
6+
7+
import com.amazonaws.lambda.durable.testing.LocalDurableTestRunner;
8+
import org.junit.jupiter.api.Test;
9+
10+
class GenericTypesExampleTest {
11+
12+
@Test
13+
void testGenericTypesExample() {
14+
var handler = new GenericTypesExample();
15+
var runner = LocalDurableTestRunner.create(GenericTypesExample.Input.class, handler::handleRequest)
16+
.withSkipTime(true);
17+
18+
var input = new GenericTypesExample.Input("user123");
19+
var result = runner.run(input);
20+
21+
assertNotNull(result);
22+
GenericTypesExample.Output output = result.getResult(GenericTypesExample.Output.class);
23+
assertNotNull(output);
24+
25+
// Verify items list
26+
assertNotNull(output.items);
27+
assertEquals(4, output.items.size());
28+
assertTrue(output.items.contains("item1"));
29+
assertTrue(output.items.contains("item4"));
30+
31+
// Verify counts map
32+
assertNotNull(output.counts);
33+
assertEquals(3, output.counts.size());
34+
assertEquals(2, output.counts.get("electronics"));
35+
assertEquals(1, output.counts.get("books"));
36+
assertEquals(1, output.counts.get("clothing"));
37+
38+
// Verify categories nested map
39+
assertNotNull(output.categories);
40+
assertEquals(3, output.categories.size());
41+
assertEquals(2, output.categories.get("electronics").size());
42+
assertTrue(output.categories.get("electronics").contains("laptop"));
43+
assertTrue(output.categories.get("electronics").contains("phone"));
44+
assertEquals(1, output.categories.get("books").size());
45+
assertTrue(output.categories.get("books").contains("fiction"));
46+
}
47+
48+
@Test
49+
void testOperationTracking() {
50+
var handler = new GenericTypesExample();
51+
var runner = LocalDurableTestRunner.create(GenericTypesExample.Input.class, handler::handleRequest)
52+
.withSkipTime(true);
53+
54+
var input = new GenericTypesExample.Input("user456");
55+
var result = runner.run(input);
56+
57+
// Verify all operations were executed
58+
var fetchItems = result.getOperation("fetch-items");
59+
assertNotNull(fetchItems);
60+
assertEquals("fetch-items", fetchItems.getName());
61+
62+
var countByCategory = result.getOperation("count-by-category");
63+
assertNotNull(countByCategory);
64+
assertEquals("count-by-category", countByCategory.getName());
65+
66+
var fetchCategories = result.getOperation("fetch-categories");
67+
assertNotNull(fetchCategories);
68+
assertEquals("fetch-categories", fetchCategories.getName());
69+
}
70+
}

examples/template.yaml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,28 @@ Resources:
142142
DockerContext: ../
143143
DockerTag: durable-examples
144144

145+
GenericTypesExampleFunction:
146+
Type: AWS::Serverless::Function
147+
Properties:
148+
PackageType: Image
149+
FunctionName: generic-types-example
150+
ImageConfig:
151+
Command: ["com.amazonaws.lambda.durable.examples.GenericTypesExample::handleRequest"]
152+
DurableConfig:
153+
ExecutionTimeout: 300
154+
RetentionPeriodInDays: 7
155+
Policies:
156+
- Statement:
157+
- Effect: Allow
158+
Action:
159+
- lambda:CheckpointDurableExecutions
160+
- lambda:GetDurableExecutionState
161+
Resource: !Sub "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:generic-types-example"
162+
Metadata:
163+
Dockerfile: examples/Dockerfile
164+
DockerContext: ../
165+
DockerTag: durable-examples
166+
145167
Outputs:
146168
SimpleStepExampleFunction:
147169
Description: Simple Step Example Function ARN
@@ -190,3 +212,11 @@ Outputs:
190212
RetryInProcessExampleFunctionName:
191213
Description: Retry In Process Example Function Name
192214
Value: !Ref RetryInProcessExampleFunction
215+
216+
GenericTypesExampleFunction:
217+
Description: Generic Types Example Function ARN
218+
Value: !GetAtt GenericTypesExampleFunction.Arn
219+
220+
GenericTypesExampleFunctionName:
221+
Description: Generic Types Example Function Name
222+
Value: !Ref GenericTypesExampleFunction

0 commit comments

Comments
 (0)