Skip to content

Commit fefb30f

Browse files
authored
Fix: Add support for List<Integer> and other primitive wrapper lists (#5)
* Fix: Add support for List<Integer> and other primitive wrapper lists Resolves #4 The annotation processor now correctly handles List<Integer>, List<Double>, and other primitive wrapper lists by generating proper DynamoDB list mapping code instead of attempting to use a non-existent ListMapper. Changes: - Added NUMBER_LIST mapping strategy to FieldInfo.MappingStrategy enum - Updated TypeAnalyzer to detect numeric element types in lists - Implemented serialization and deserialization code generation for NUMBER_LIST - Added comprehensive tests for List<Integer> mapping The generated mappers now directly convert primitive wrapper lists to DynamoDB's native list format (L) with number attributes (N). * Add core implementation for List<Integer> support - Added NUMBER_LIST mapping strategy to FieldInfo.MappingStrategy enum - Updated TypeAnalyzer to detect numeric element types in lists - Implemented serialization and deserialization in FieldMappingCodeGenerator
1 parent 8c1be4f commit fefb30f

File tree

5 files changed

+198
-1
lines changed

5 files changed

+198
-1
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.github.wassertim.dynamodb.toolkit.integration.entities;
2+
3+
import com.github.wassertim.dynamodb.toolkit.api.annotations.DynamoMappable;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Builder;
6+
import lombok.Data;
7+
import lombok.NoArgsConstructor;
8+
9+
import java.util.List;
10+
11+
@DynamoMappable
12+
@Data
13+
@NoArgsConstructor
14+
@AllArgsConstructor
15+
@Builder
16+
public class RouteInstruction {
17+
private String text;
18+
private Double distance;
19+
private Double duration;
20+
private String type;
21+
22+
// This field causes the generation error
23+
private List<Integer> waypointIndices;
24+
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package com.github.wassertim.dynamodb.toolkit.integration;
2+
3+
import org.junit.jupiter.api.Test;
4+
import org.junit.jupiter.api.BeforeEach;
5+
import org.junit.jupiter.api.DisplayName;
6+
import static org.assertj.core.api.Assertions.*;
7+
8+
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
9+
import com.github.wassertim.dynamodb.toolkit.integration.entities.RouteInstruction;
10+
import com.github.wassertim.dynamodb.toolkit.mappers.RouteInstructionMapper;
11+
12+
import java.util.Arrays;
13+
import java.util.Map;
14+
15+
/**
16+
* Integration test to verify that the DynamoDB Toolkit correctly handles
17+
* List<Integer> fields in @DynamoMappable entities.
18+
*/
19+
public class ListIntegerMappingTest {
20+
21+
private RouteInstruction routeInstruction;
22+
private RouteInstructionMapper mapper;
23+
24+
@BeforeEach
25+
void setUp() {
26+
routeInstruction = RouteInstruction.builder()
27+
.text("Turn left on Main Street")
28+
.distance(150.5)
29+
.duration(45.0)
30+
.type("turn")
31+
.waypointIndices(Arrays.asList(0, 3, 7, 12))
32+
.build();
33+
34+
mapper = new RouteInstructionMapper();
35+
}
36+
37+
@Test
38+
@DisplayName("List<Integer> field should be correctly mapped to DynamoDB")
39+
void testListIntegerMapping() {
40+
// Convert to DynamoDB AttributeValue
41+
AttributeValue attributeValue = mapper.toDynamoDbAttributeValue(routeInstruction);
42+
43+
assertThat(attributeValue).isNotNull();
44+
assertThat(attributeValue.hasM()).isTrue();
45+
46+
Map<String, AttributeValue> map = attributeValue.m();
47+
48+
// Verify all fields are present
49+
assertThat(map).containsKey("text");
50+
assertThat(map).containsKey("distance");
51+
assertThat(map).containsKey("duration");
52+
assertThat(map).containsKey("type");
53+
assertThat(map).containsKey("waypointIndices");
54+
55+
// Verify waypointIndices is a list
56+
AttributeValue waypointIndicesValue = map.get("waypointIndices");
57+
assertThat(waypointIndicesValue.hasL()).isTrue();
58+
assertThat(waypointIndicesValue.l()).hasSize(4);
59+
60+
// Verify each element is a number
61+
assertThat(waypointIndicesValue.l().get(0).n()).isEqualTo("0");
62+
assertThat(waypointIndicesValue.l().get(1).n()).isEqualTo("3");
63+
assertThat(waypointIndicesValue.l().get(2).n()).isEqualTo("7");
64+
assertThat(waypointIndicesValue.l().get(3).n()).isEqualTo("12");
65+
}
66+
67+
@Test
68+
@DisplayName("List<Integer> field should support round-trip conversion")
69+
void testListIntegerRoundTrip() {
70+
// Convert to DynamoDB and back
71+
AttributeValue attributeValue = mapper.toDynamoDbAttributeValue(routeInstruction);
72+
RouteInstruction converted = mapper.fromDynamoDbAttributeValue(attributeValue);
73+
74+
assertThat(converted).isNotNull();
75+
assertThat(converted.getText()).isEqualTo(routeInstruction.getText());
76+
assertThat(converted.getDistance()).isEqualTo(routeInstruction.getDistance());
77+
assertThat(converted.getDuration()).isEqualTo(routeInstruction.getDuration());
78+
assertThat(converted.getType()).isEqualTo(routeInstruction.getType());
79+
assertThat(converted.getWaypointIndices()).isEqualTo(routeInstruction.getWaypointIndices());
80+
}
81+
82+
@Test
83+
@DisplayName("Null List<Integer> field should be handled correctly")
84+
void testNullListInteger() {
85+
RouteInstruction withNullList = RouteInstruction.builder()
86+
.text("Continue straight")
87+
.distance(200.0)
88+
.duration(60.0)
89+
.type("straight")
90+
.waypointIndices(null)
91+
.build();
92+
93+
AttributeValue attributeValue = mapper.toDynamoDbAttributeValue(withNullList);
94+
assertThat(attributeValue).isNotNull();
95+
96+
Map<String, AttributeValue> map = attributeValue.m();
97+
assertThat(map).doesNotContainKey("waypointIndices");
98+
99+
// Round-trip conversion
100+
RouteInstruction converted = mapper.fromDynamoDbAttributeValue(attributeValue);
101+
assertThat(converted.getWaypointIndices()).isNull();
102+
}
103+
104+
@Test
105+
@DisplayName("Empty List<Integer> field should be handled correctly")
106+
void testEmptyListInteger() {
107+
RouteInstruction withEmptyList = RouteInstruction.builder()
108+
.text("Arrive at destination")
109+
.distance(0.0)
110+
.duration(0.0)
111+
.type("arrive")
112+
.waypointIndices(Arrays.asList())
113+
.build();
114+
115+
AttributeValue attributeValue = mapper.toDynamoDbAttributeValue(withEmptyList);
116+
assertThat(attributeValue).isNotNull();
117+
118+
Map<String, AttributeValue> map = attributeValue.m();
119+
assertThat(map).containsKey("waypointIndices");
120+
121+
AttributeValue waypointIndicesValue = map.get("waypointIndices");
122+
assertThat(waypointIndicesValue.hasL()).isTrue();
123+
assertThat(waypointIndicesValue.l()).isEmpty();
124+
125+
// Round-trip conversion
126+
RouteInstruction converted = mapper.fromDynamoDbAttributeValue(attributeValue);
127+
assertThat(converted.getWaypointIndices()).isEmpty();
128+
}
129+
}

src/main/java/com/github/wassertim/dynamodb/toolkit/analysis/FieldInfo.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ public enum MappingStrategy {
7272
INSTANT, // Instant timestamp mapping
7373
ENUM, // Enum name mapping
7474
STRING_LIST, // List<String> mapping
75+
NUMBER_LIST, // List<Integer>, List<Double>, etc. mapping
7576
NESTED_NUMBER_LIST, // List<List<Double>> mapping for coordinates
7677
COMPLEX_OBJECT, // Nested object requiring mapper
7778
COMPLEX_LIST, // List<ComplexObject> requiring mapper

src/main/java/com/github/wassertim/dynamodb/toolkit/analysis/TypeAnalyzer.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,11 @@ private FieldInfo.MappingStrategy determineMappingStrategy(TypeMirror fieldType)
119119
if (isListType(fieldType)) {
120120
TypeMirror elementType = getListElementType(fieldType);
121121
if (elementType != null) {
122-
String elementTypeName = elementType.toString();
122+
String elementTypeName = getCleanTypeName(elementType);
123123
if (isStringType(elementTypeName)) {
124124
return FieldInfo.MappingStrategy.STRING_LIST;
125+
} else if (isNumberType(elementTypeName) || isNumericPrimitive(elementType)) {
126+
return FieldInfo.MappingStrategy.NUMBER_LIST;
125127
} else if (isNestedNumberList(elementType)) {
126128
return FieldInfo.MappingStrategy.NESTED_NUMBER_LIST;
127129
} else if (hasDynamoMappableAnnotation(elementType)) {

src/main/java/com/github/wassertim/dynamodb/toolkit/mapping/FieldMappingCodeGenerator.java

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ public CodeBlock generateToAttributeValueMapping(FieldInfo field, String objectN
3838
case INSTANT -> generateInstantMapping(fieldName, getterCall);
3939
case ENUM -> generateEnumMapping(fieldName, getterCall);
4040
case STRING_LIST -> generateStringListMapping(fieldName, getterCall);
41+
case NUMBER_LIST -> generateNumberListMapping(fieldName, getterCall);
4142
case NESTED_NUMBER_LIST -> generateNestedNumberListMapping(fieldName, getterCall);
4243
case COMPLEX_OBJECT -> generateComplexObjectMapping(field, fieldName, getterCall);
4344
case COMPLEX_LIST -> generateComplexListMapping(field, fieldName, getterCall);
@@ -118,6 +119,21 @@ private CodeBlock generateStringListMapping(String fieldName, String getterCall)
118119
.build();
119120
}
120121

122+
private CodeBlock generateNumberListMapping(String fieldName, String getterCall) {
123+
ClassName attributeValue = ClassName.get(AttributeValue.class);
124+
ClassName list = ClassName.get(List.class);
125+
ClassName collectors = ClassName.get(Collectors.class);
126+
127+
return CodeBlock.builder()
128+
.beginControlFlow("if ($L)", utils.createNullCheck(getterCall))
129+
.add("$T<$T> $LList = $L.stream()\n", list, attributeValue, fieldName, getterCall)
130+
.add(" .map(val -> $T.builder().n($T.valueOf(val)).build())\n", attributeValue, String.class)
131+
.addStatement(" .collect($T.toList())", collectors)
132+
.addStatement("$L", utils.createAttributePut(fieldName, utils.createListAttribute(fieldName + "List")))
133+
.endControlFlow()
134+
.build();
135+
}
136+
121137
private CodeBlock generateNestedNumberListMapping(String fieldName, String getterCall) {
122138
ClassName attributeValue = ClassName.get(AttributeValue.class);
123139
ClassName list = ClassName.get(List.class);
@@ -193,6 +209,7 @@ public CodeBlock generateFromAttributeValueMapping(FieldInfo field) {
193209
case INSTANT -> generateInstantDeserialization(fieldName);
194210
case ENUM -> generateEnumDeserialization(field, fieldName);
195211
case STRING_LIST -> generateStringListDeserialization(fieldName);
212+
case NUMBER_LIST -> generateNumberListDeserialization(field, fieldName);
196213
case NESTED_NUMBER_LIST -> generateNestedNumberListDeserialization(fieldName);
197214
case COMPLEX_OBJECT -> generateComplexObjectDeserialization(field, fieldName);
198215
case COMPLEX_LIST -> generateComplexListDeserialization(field, fieldName);
@@ -280,6 +297,30 @@ private CodeBlock generateStringListDeserialization(String fieldName) {
280297
.build();
281298
}
282299

300+
private CodeBlock generateNumberListDeserialization(FieldInfo field, String fieldName) {
301+
ClassName mappingUtils = ClassName.get("com.github.wassertim.dynamodb.runtime", "MappingUtils");
302+
ClassName attributeValue = ClassName.get(AttributeValue.class);
303+
ClassName list = ClassName.get(List.class);
304+
ClassName objects = ClassName.get(Objects.class);
305+
ClassName collectors = ClassName.get(Collectors.class);
306+
307+
// Determine the element type from the field
308+
String elementTypeQualified = utils.extractListElementQualifiedType(field);
309+
String numericMethod = utils.getNumericMethodForType(elementTypeQualified);
310+
String javaType = utils.getJavaTypeForNumeric(elementTypeQualified);
311+
312+
return CodeBlock.builder()
313+
.addStatement("$T<$T> listValue = $T.getListSafely($LAttr)", list, attributeValue, mappingUtils, fieldName)
314+
.beginControlFlow("if (listValue != null)")
315+
.add("$T<$L> $LList = listValue.stream()\n", list, javaType, fieldName)
316+
.add(" .map($T::$L)\n", mappingUtils, numericMethod)
317+
.add(" .filter($T::nonNull)\n", objects)
318+
.addStatement(" .collect($T.toList())", collectors)
319+
.addStatement("builder.$L($LList)", fieldName, fieldName)
320+
.endControlFlow()
321+
.build();
322+
}
323+
283324
private CodeBlock generateNestedNumberListDeserialization(String fieldName) {
284325
ClassName mappingUtils = ClassName.get("com.github.wassertim.dynamodb.runtime", "MappingUtils");
285326
ClassName attributeValue = ClassName.get(AttributeValue.class);

0 commit comments

Comments
 (0)