diff --git a/hll/dynamodb-mapper/dynamodb-mapper-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/model/MapperTypes.kt b/hll/dynamodb-mapper/dynamodb-mapper-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/model/MapperTypes.kt index ef179e20a07..6c93e1001c4 100644 --- a/hll/dynamodb-mapper/dynamodb-mapper-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/model/MapperTypes.kt +++ b/hll/dynamodb-mapper/dynamodb-mapper-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/model/MapperTypes.kt @@ -78,6 +78,7 @@ public object MapperTypes { public object Values { public fun valueConverter(value: Type): TypeRef = TypeRef(MapperPkg.Hl.Values, "ValueConverter", genericArgs = listOf(value)) public val ItemToValueConverter: TypeRef = TypeRef(MapperPkg.Hl.Values, "ItemToValueConverter") + public val NullableConverter: TypeRef = TypeRef(MapperPkg.Hl.Values, "NullableConverter") public object Collections { public val ListConverter: TypeRef = TypeRef(MapperPkg.Hl.CollectionValues, "ListConverter") diff --git a/hll/dynamodb-mapper/dynamodb-mapper-schema-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/annotations/rendering/SchemaRenderer.kt b/hll/dynamodb-mapper/dynamodb-mapper-schema-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/annotations/rendering/SchemaRenderer.kt index 81dca0d50a6..9655c3dccca 100644 --- a/hll/dynamodb-mapper/dynamodb-mapper-schema-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/annotations/rendering/SchemaRenderer.kt +++ b/hll/dynamodb-mapper/dynamodb-mapper-schema-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/annotations/rendering/SchemaRenderer.kt @@ -23,6 +23,7 @@ import com.google.devtools.ksp.symbol.* * @param classDeclaration the [KSClassDeclaration] of the class * @param ctx the [RenderContext] of the renderer */ +@OptIn(KspExperimental::class) internal class SchemaRenderer( private val classDeclaration: KSClassDeclaration, private val ctx: RenderContext, @@ -34,7 +35,6 @@ internal class SchemaRenderer( private val converterName = "${className}Converter" private val schemaName = "${className}Schema" - @OptIn(KspExperimental::class) private val dynamoDbItemAnnotation = classDeclaration.getAnnotationsByType(DynamoDbItem::class).single() private val itemConverter: Type = dynamoDbItemAnnotation @@ -46,7 +46,6 @@ internal class SchemaRenderer( TypeRef(pkg, shortName) } ?: TypeRef(ctx.pkg, converterName) - @OptIn(KspExperimental::class) private val properties = classDeclaration .getAllProperties() .filterNot { it.modifiers.contains(Modifier.PRIVATE) || it.isAnnotationPresent(DynamoDbIgnore::class) } @@ -61,7 +60,14 @@ internal class SchemaRenderer( } private val partitionKeyProp = properties.single { it.isPk } + private val partitionKeyName = partitionKeyProp + .getAnnotationsByType(DynamoDbAttribute::class) + .singleOrNull()?.name ?: partitionKeyProp.name + private val sortKeyProp = properties.singleOrNull { it.isSk } + private val sortKeyName = sortKeyProp + ?.getAnnotationsByType(DynamoDbAttribute::class) + ?.singleOrNull()?.name ?: sortKeyProp?.name /** * Skip rendering a class builder if: @@ -195,6 +201,12 @@ internal class SchemaRenderer( type.isGenericFor(Types.Kotlin.Collections.Set) -> writeInline("#T", ksType.singleArgument().setValueConverter) + type.nullable -> { + writeInline("#T(", MapperTypes.Values.NullableConverter) + renderValueConverter(ksType.makeNotNullable()) + writeInline(")") + } + else -> writeInline( "#T", when (type) { @@ -218,7 +230,7 @@ internal class SchemaRenderer( Types.Kotlin.UShort -> MapperTypes.Values.Scalars.UShortConverter Types.Kotlin.ULong -> MapperTypes.Values.Scalars.ULongConverter - else -> error("Unsupported attribute type $this") + else -> error("Unsupported attribute type $type") }, ) } @@ -280,9 +292,9 @@ internal class SchemaRenderer( write("@#T", Types.Smithy.ExperimentalApi) withBlock("#Lobject #L : #T {", "}", ctx.attributes.visibility, schemaName, schemaType) { write("override val converter : #1T = #1T", itemConverter) - write("override val partitionKey: #T = #T(#S)", MapperTypes.Items.keySpec(partitionKeyProp.keySpec), partitionKeyProp.keySpecType, partitionKeyProp.name) + write("override val partitionKey: #T = #T(#S)", MapperTypes.Items.keySpec(partitionKeyProp.keySpec), partitionKeyProp.keySpecType, partitionKeyName) if (sortKeyProp != null) { - write("override val sortKey: #T = #T(#S)", MapperTypes.Items.keySpec(sortKeyProp.keySpec), sortKeyProp.keySpecType, sortKeyProp.name) + write("override val sortKey: #T = #T(#S)", MapperTypes.Items.keySpec(sortKeyProp.keySpec), sortKeyProp.keySpecType, sortKeyName!!) } } blankLine() diff --git a/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/plugins/SchemaGeneratorPluginTest.kt b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/plugins/SchemaGeneratorPluginTest.kt index 9df663251e4..efdf2aa35d6 100644 --- a/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/plugins/SchemaGeneratorPluginTest.kt +++ b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/plugins/SchemaGeneratorPluginTest.kt @@ -448,6 +448,33 @@ class SchemaGeneratorPluginTest { assertContains(setOf(TaskOutcome.SUCCESS, TaskOutcome.UP_TO_DATE), testResult.task(":test")?.outcome) } + @Test + fun testNullableTypes() { + buildFile.appendText( + """ + dependencies { + implementation("aws.smithy.kotlin:runtime-core:$smithyKotlinVersion") + testImplementation(kotlin("test")) + } + """.trimIndent(), + ) + + createClassFile("standard-item-converters/src/NullableItem") + + val buildResult = runner.build() + assertContains(setOf(TaskOutcome.SUCCESS, TaskOutcome.UP_TO_DATE), buildResult.task(":build")?.outcome) + val schemaFile = File(testProjectDir, "build/generated/ksp/main/kotlin/org/example/dynamodbmapper/generatedschemas/NullableItemSchema.kt") + assertTrue(schemaFile.exists()) + + val testFile = File(testProjectDir, "src/test/kotlin/org/example/standard-item-converters/test/NullableItemTest.kt") + testFile.ensureParentDirsCreated() + testFile.createNewFile() + testFile.writeText(getResource("/standard-item-converters/test/NullableItemTest.kt")) + + val testResult = runner.withArguments("test").build() + assertContains(setOf(TaskOutcome.SUCCESS, TaskOutcome.UP_TO_DATE), testResult.task(":test")?.outcome) + } + @Test fun testLists() { buildFile.appendText( @@ -525,4 +552,28 @@ class SchemaGeneratorPluginTest { val testResult = runner.withArguments("test").build() assertContains(setOf(TaskOutcome.SUCCESS, TaskOutcome.UP_TO_DATE), testResult.task(":test")?.outcome) } + + @Test + fun testRenamedPartitionKey() { + createClassFile("RenamedPartitionKey") + + val result = runner.build() + assertContains(setOf(TaskOutcome.SUCCESS, TaskOutcome.UP_TO_DATE), result.task(":build")?.outcome) + + val schemaFile = File(testProjectDir, "build/generated/ksp/main/kotlin/org/example/dynamodbmapper/generatedschemas/RenamedPartitionKeySchema.kt") + assertTrue(schemaFile.exists()) + + val schemaContents = schemaFile.readText() + + // Schema should use the renamed partition key + assertContains( + schemaContents, + """ + object RenamedPartitionKeySchema : ItemSchema.PartitionKey { + override val converter : RenamedPartitionKeyConverter = RenamedPartitionKeyConverter + override val partitionKey: KeySpec = aws.sdk.kotlin.hll.dynamodbmapper.items.KeySpec.Number("user_id") + } + """.trimIndent(), + ) + } } diff --git a/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/RenamedPartitionKey.kt b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/RenamedPartitionKey.kt new file mode 100644 index 00000000000..e507c9af815 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/RenamedPartitionKey.kt @@ -0,0 +1,16 @@ +package org.example + +import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbAttribute +import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbItem +import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbPartitionKey + +@DynamoDbItem +public data class RenamedPartitionKey( + @DynamoDbPartitionKey + @DynamoDbAttribute("user_id") + var id: Int, + + @DynamoDbAttribute("fName") var givenName: String, + @DynamoDbAttribute("lName") var surname: String, + var age: Int, +) diff --git a/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/standard-item-converters/src/NullableItem.kt b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/standard-item-converters/src/NullableItem.kt new file mode 100644 index 00000000000..62576330896 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/standard-item-converters/src/NullableItem.kt @@ -0,0 +1,35 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package org.example + +import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbItem +import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbPartitionKey +import aws.smithy.kotlin.runtime.time.Instant + +@DynamoDbItem +public data class NullableItem( + @DynamoDbPartitionKey var id: Int, + + /** + * A selection of nullable types + */ + var string: String?, + var byte: Byte?, + var int: Int?, + var instant: Instant?, +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is NullableItem) return false + + if (id != other.id) return false + if (string != other.string) return false + if (byte != other.byte) return false + if (int != other.int) return false + if (instant?.epochSeconds != other.instant?.epochSeconds) return false + + return true + } +} diff --git a/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/standard-item-converters/test/NullableItemTest.kt b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/standard-item-converters/test/NullableItemTest.kt new file mode 100644 index 00000000000..1c5b89d5a90 --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/standard-item-converters/test/NullableItemTest.kt @@ -0,0 +1,29 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package org.example + +import aws.smithy.kotlin.runtime.ExperimentalApi +import aws.smithy.kotlin.runtime.time.Instant +import org.example.dynamodbmapper.generatedschemas.NullableItemConverter +import kotlin.test.Test +import kotlin.test.assertEquals + +@OptIn(ExperimentalApi::class) +public class NullableItemTest { + @Test + fun converterTest() { + val nullable = NullableItem( + id = 1, + string = null, + byte = null, + int = 5, + instant = Instant.now(), + ) + + val item = NullableItemConverter.convertTo(nullable) + val converted = NullableItemConverter.convertFrom(item) + assertEquals(nullable, converted) + } +}