Skip to content
Merged
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
@@ -0,0 +1,24 @@
package com.github.wassertim.dynamodb.toolkit.integration.entities;

import com.github.wassertim.dynamodb.toolkit.api.annotations.DynamoMappable;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

@DynamoMappable
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class RouteInstruction {
private String text;
private Double distance;
private Double duration;
private String type;

// This field causes the generation error
private List<Integer> waypointIndices;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package com.github.wassertim.dynamodb.toolkit.integration;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import static org.assertj.core.api.Assertions.*;

import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
import com.github.wassertim.dynamodb.toolkit.integration.entities.RouteInstruction;
import com.github.wassertim.dynamodb.toolkit.mappers.RouteInstructionMapper;

import java.util.Arrays;
import java.util.Map;

/**
* Integration test to verify that the DynamoDB Toolkit correctly handles
* List<Integer> fields in @DynamoMappable entities.
*/
public class ListIntegerMappingTest {

private RouteInstruction routeInstruction;
private RouteInstructionMapper mapper;

@BeforeEach
void setUp() {
routeInstruction = RouteInstruction.builder()
.text("Turn left on Main Street")
.distance(150.5)
.duration(45.0)
.type("turn")
.waypointIndices(Arrays.asList(0, 3, 7, 12))
.build();

mapper = new RouteInstructionMapper();
}

@Test
@DisplayName("List<Integer> field should be correctly mapped to DynamoDB")
void testListIntegerMapping() {
// Convert to DynamoDB AttributeValue
AttributeValue attributeValue = mapper.toDynamoDbAttributeValue(routeInstruction);

assertThat(attributeValue).isNotNull();
assertThat(attributeValue.hasM()).isTrue();

Map<String, AttributeValue> map = attributeValue.m();

// Verify all fields are present
assertThat(map).containsKey("text");
assertThat(map).containsKey("distance");
assertThat(map).containsKey("duration");
assertThat(map).containsKey("type");
assertThat(map).containsKey("waypointIndices");

// Verify waypointIndices is a list
AttributeValue waypointIndicesValue = map.get("waypointIndices");
assertThat(waypointIndicesValue.hasL()).isTrue();
assertThat(waypointIndicesValue.l()).hasSize(4);

// Verify each element is a number
assertThat(waypointIndicesValue.l().get(0).n()).isEqualTo("0");
assertThat(waypointIndicesValue.l().get(1).n()).isEqualTo("3");
assertThat(waypointIndicesValue.l().get(2).n()).isEqualTo("7");
assertThat(waypointIndicesValue.l().get(3).n()).isEqualTo("12");
}

@Test
@DisplayName("List<Integer> field should support round-trip conversion")
void testListIntegerRoundTrip() {
// Convert to DynamoDB and back
AttributeValue attributeValue = mapper.toDynamoDbAttributeValue(routeInstruction);
RouteInstruction converted = mapper.fromDynamoDbAttributeValue(attributeValue);

assertThat(converted).isNotNull();
assertThat(converted.getText()).isEqualTo(routeInstruction.getText());
assertThat(converted.getDistance()).isEqualTo(routeInstruction.getDistance());
assertThat(converted.getDuration()).isEqualTo(routeInstruction.getDuration());
assertThat(converted.getType()).isEqualTo(routeInstruction.getType());
assertThat(converted.getWaypointIndices()).isEqualTo(routeInstruction.getWaypointIndices());
}

@Test
@DisplayName("Null List<Integer> field should be handled correctly")
void testNullListInteger() {
RouteInstruction withNullList = RouteInstruction.builder()
.text("Continue straight")
.distance(200.0)
.duration(60.0)
.type("straight")
.waypointIndices(null)
.build();

AttributeValue attributeValue = mapper.toDynamoDbAttributeValue(withNullList);
assertThat(attributeValue).isNotNull();

Map<String, AttributeValue> map = attributeValue.m();
assertThat(map).doesNotContainKey("waypointIndices");

// Round-trip conversion
RouteInstruction converted = mapper.fromDynamoDbAttributeValue(attributeValue);
assertThat(converted.getWaypointIndices()).isNull();
}

@Test
@DisplayName("Empty List<Integer> field should be handled correctly")
void testEmptyListInteger() {
RouteInstruction withEmptyList = RouteInstruction.builder()
.text("Arrive at destination")
.distance(0.0)
.duration(0.0)
.type("arrive")
.waypointIndices(Arrays.asList())
.build();

AttributeValue attributeValue = mapper.toDynamoDbAttributeValue(withEmptyList);
assertThat(attributeValue).isNotNull();

Map<String, AttributeValue> map = attributeValue.m();
assertThat(map).containsKey("waypointIndices");

AttributeValue waypointIndicesValue = map.get("waypointIndices");
assertThat(waypointIndicesValue.hasL()).isTrue();
assertThat(waypointIndicesValue.l()).isEmpty();

// Round-trip conversion
RouteInstruction converted = mapper.fromDynamoDbAttributeValue(attributeValue);
assertThat(converted.getWaypointIndices()).isEmpty();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ public enum MappingStrategy {
INSTANT, // Instant timestamp mapping
ENUM, // Enum name mapping
STRING_LIST, // List<String> mapping
NUMBER_LIST, // List<Integer>, List<Double>, etc. mapping
NESTED_NUMBER_LIST, // List<List<Double>> mapping for coordinates
COMPLEX_OBJECT, // Nested object requiring mapper
COMPLEX_LIST, // List<ComplexObject> requiring mapper
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,11 @@ private FieldInfo.MappingStrategy determineMappingStrategy(TypeMirror fieldType)
if (isListType(fieldType)) {
TypeMirror elementType = getListElementType(fieldType);
if (elementType != null) {
String elementTypeName = elementType.toString();
String elementTypeName = getCleanTypeName(elementType);
if (isStringType(elementTypeName)) {
return FieldInfo.MappingStrategy.STRING_LIST;
} else if (isNumberType(elementTypeName) || isNumericPrimitive(elementType)) {
return FieldInfo.MappingStrategy.NUMBER_LIST;
} else if (isNestedNumberList(elementType)) {
return FieldInfo.MappingStrategy.NESTED_NUMBER_LIST;
} else if (hasDynamoMappableAnnotation(elementType)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public CodeBlock generateToAttributeValueMapping(FieldInfo field, String objectN
case INSTANT -> generateInstantMapping(fieldName, getterCall);
case ENUM -> generateEnumMapping(fieldName, getterCall);
case STRING_LIST -> generateStringListMapping(fieldName, getterCall);
case NUMBER_LIST -> generateNumberListMapping(fieldName, getterCall);
case NESTED_NUMBER_LIST -> generateNestedNumberListMapping(fieldName, getterCall);
case COMPLEX_OBJECT -> generateComplexObjectMapping(field, fieldName, getterCall);
case COMPLEX_LIST -> generateComplexListMapping(field, fieldName, getterCall);
Expand Down Expand Up @@ -118,6 +119,21 @@ private CodeBlock generateStringListMapping(String fieldName, String getterCall)
.build();
}

private CodeBlock generateNumberListMapping(String fieldName, String getterCall) {
ClassName attributeValue = ClassName.get(AttributeValue.class);
ClassName list = ClassName.get(List.class);
ClassName collectors = ClassName.get(Collectors.class);

return CodeBlock.builder()
.beginControlFlow("if ($L)", utils.createNullCheck(getterCall))
.add("$T<$T> $LList = $L.stream()\n", list, attributeValue, fieldName, getterCall)
.add(" .map(val -> $T.builder().n($T.valueOf(val)).build())\n", attributeValue, String.class)
.addStatement(" .collect($T.toList())", collectors)
.addStatement("$L", utils.createAttributePut(fieldName, utils.createListAttribute(fieldName + "List")))
.endControlFlow()
.build();
}

private CodeBlock generateNestedNumberListMapping(String fieldName, String getterCall) {
ClassName attributeValue = ClassName.get(AttributeValue.class);
ClassName list = ClassName.get(List.class);
Expand Down Expand Up @@ -193,6 +209,7 @@ public CodeBlock generateFromAttributeValueMapping(FieldInfo field) {
case INSTANT -> generateInstantDeserialization(fieldName);
case ENUM -> generateEnumDeserialization(field, fieldName);
case STRING_LIST -> generateStringListDeserialization(fieldName);
case NUMBER_LIST -> generateNumberListDeserialization(field, fieldName);
case NESTED_NUMBER_LIST -> generateNestedNumberListDeserialization(fieldName);
case COMPLEX_OBJECT -> generateComplexObjectDeserialization(field, fieldName);
case COMPLEX_LIST -> generateComplexListDeserialization(field, fieldName);
Expand Down Expand Up @@ -280,6 +297,30 @@ private CodeBlock generateStringListDeserialization(String fieldName) {
.build();
}

private CodeBlock generateNumberListDeserialization(FieldInfo field, String fieldName) {
ClassName mappingUtils = ClassName.get("com.github.wassertim.dynamodb.runtime", "MappingUtils");
ClassName attributeValue = ClassName.get(AttributeValue.class);
ClassName list = ClassName.get(List.class);
ClassName objects = ClassName.get(Objects.class);
ClassName collectors = ClassName.get(Collectors.class);

// Determine the element type from the field
String elementTypeQualified = utils.extractListElementQualifiedType(field);
String numericMethod = utils.getNumericMethodForType(elementTypeQualified);
String javaType = utils.getJavaTypeForNumeric(elementTypeQualified);

return CodeBlock.builder()
.addStatement("$T<$T> listValue = $T.getListSafely($LAttr)", list, attributeValue, mappingUtils, fieldName)
.beginControlFlow("if (listValue != null)")
.add("$T<$L> $LList = listValue.stream()\n", list, javaType, fieldName)
.add(" .map($T::$L)\n", mappingUtils, numericMethod)
.add(" .filter($T::nonNull)\n", objects)
.addStatement(" .collect($T.toList())", collectors)
.addStatement("builder.$L($LList)", fieldName, fieldName)
.endControlFlow()
.build();
}

private CodeBlock generateNestedNumberListDeserialization(String fieldName) {
ClassName mappingUtils = ClassName.get("com.github.wassertim.dynamodb.runtime", "MappingUtils");
ClassName attributeValue = ClassName.get(AttributeValue.class);
Expand Down