diff --git a/hll/dynamodb-mapper/dynamodb-mapper-annotations/api/dynamodb-mapper-annotations.api b/hll/dynamodb-mapper/dynamodb-mapper-annotations/api/dynamodb-mapper-annotations.api index 98bcd3deee7..8f5c7313892 100644 --- a/hll/dynamodb-mapper/dynamodb-mapper-annotations/api/dynamodb-mapper-annotations.api +++ b/hll/dynamodb-mapper/dynamodb-mapper-annotations/api/dynamodb-mapper-annotations.api @@ -2,7 +2,11 @@ public abstract interface annotation class aws/sdk/kotlin/hll/dynamodbmapper/Dyn public abstract fun name ()Ljava/lang/String; } +public abstract interface annotation class aws/sdk/kotlin/hll/dynamodbmapper/DynamoDbIgnore : java/lang/annotation/Annotation { +} + public abstract interface annotation class aws/sdk/kotlin/hll/dynamodbmapper/DynamoDbItem : java/lang/annotation/Annotation { + public abstract fun converterName ()Ljava/lang/String; } public abstract interface annotation class aws/sdk/kotlin/hll/dynamodbmapper/DynamoDbPartitionKey : java/lang/annotation/Annotation { diff --git a/hll/dynamodb-mapper/dynamodb-mapper-annotations/common/src/aws/sdk/kotlin/hll/dynamodbmapper/Annotations.kt b/hll/dynamodb-mapper/dynamodb-mapper-annotations/common/src/aws/sdk/kotlin/hll/dynamodbmapper/Annotations.kt index 0bef80bcf83..e102c7f49c4 100644 --- a/hll/dynamodb-mapper/dynamodb-mapper-annotations/common/src/aws/sdk/kotlin/hll/dynamodbmapper/Annotations.kt +++ b/hll/dynamodb-mapper/dynamodb-mapper-annotations/common/src/aws/sdk/kotlin/hll/dynamodbmapper/Annotations.kt @@ -12,11 +12,14 @@ package aws.sdk.kotlin.hll.dynamodbmapper public annotation class DynamoDbAttribute(val name: String) /** - * Specifies that this class/interface describes an item type in a table. All properties of this type will be mapped to + * Specifies that this class/interface describes an item type in a table. All public properties of this type will be mapped to * attributes unless they are explicitly ignored. + * @param converterName The fully qualified name of the item converter to be used for converting this class/interface. + * If not set, one will be automatically generated. */ +// FIXME Update to take a KClass, which will require splitting codegen modules due to a circular dependency @Target(AnnotationTarget.CLASS) -public annotation class DynamoDbItem +public annotation class DynamoDbItem(val converterName: String = "") /** * Specifies that this property is the primary key for the item. Every top-level [DynamoDbItem] to be used in a table @@ -31,3 +34,9 @@ public annotation class DynamoDbPartitionKey */ @Target(AnnotationTarget.PROPERTY) public annotation class DynamoDbSortKey + +/** + * Specifies that this property should be ignored during mapping. + */ +@Target(AnnotationTarget.PROPERTY) +public annotation class DynamoDbIgnore diff --git a/hll/dynamodb-mapper/dynamodb-mapper-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/annotations/rendering/SchemaRenderer.kt b/hll/dynamodb-mapper/dynamodb-mapper-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/annotations/rendering/SchemaRenderer.kt index 71efcd19e36..7114762aa44 100644 --- a/hll/dynamodb-mapper/dynamodb-mapper-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/annotations/rendering/SchemaRenderer.kt +++ b/hll/dynamodb-mapper/dynamodb-mapper-codegen/src/main/kotlin/aws/sdk/kotlin/hll/dynamodbmapper/codegen/annotations/rendering/SchemaRenderer.kt @@ -10,9 +10,7 @@ import aws.sdk.kotlin.hll.codegen.model.TypeRef import aws.sdk.kotlin.hll.codegen.model.Types import aws.sdk.kotlin.hll.codegen.rendering.* import aws.sdk.kotlin.hll.codegen.util.visibility -import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbAttribute -import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbPartitionKey -import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbSortKey +import aws.sdk.kotlin.hll.dynamodbmapper.* import aws.sdk.kotlin.hll.dynamodbmapper.codegen.annotations.AnnotationsProcessorOptions import aws.sdk.kotlin.hll.dynamodbmapper.codegen.annotations.GenerateBuilderClasses import aws.sdk.kotlin.hll.dynamodbmapper.codegen.model.MapperTypes @@ -42,7 +40,23 @@ internal class SchemaRenderer( private val converterName = "${className}Converter" private val schemaName = "${className}Schema" - private val properties = classDeclaration.getAllProperties().filterNot { it.modifiers.contains(Modifier.PRIVATE) } + @OptIn(KspExperimental::class) + private val dynamoDbItemAnnotation = classDeclaration.getAnnotationsByType(DynamoDbItem::class).single() + + private val itemConverter: Type = dynamoDbItemAnnotation + .converterName + .takeIf { it.isNotBlank() } + ?.let { + val pkg = it.substringBeforeLast(".") + val shortName = it.removePrefix("$pkg.") + 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) } + private val annotatedProperties = properties.mapNotNull(AnnotatedClassProperty.Companion::from) init { @@ -58,7 +72,7 @@ internal class SchemaRenderer( private val sortKeyProp = annotatedProperties.singleOrNull { it.isSk } /** - * We skip rendering a class builder if: + * Skip rendering a class builder if: * - the user has configured GenerateBuilders to WHEN_REQUIRED (default value) AND * - the class has all mutable members AND * - the class has a zero-arg constructor @@ -75,8 +89,13 @@ internal class SchemaRenderer( if (shouldRenderBuilder) { renderBuilder() } - renderItemConverter() + + if (dynamoDbItemAnnotation.converterName.isBlank()) { + renderItemConverter() + } + renderSchema() + if (ctx.attributes[AnnotationsProcessorOptions.GenerateGetTableMethodAttribute]) { renderGetTable() } @@ -140,7 +159,7 @@ internal class SchemaRenderer( } withBlock("#Lobject #L : #T {", "}", ctx.attributes.visibility, schemaName, schemaType) { - write("override val converter : #1L = #1L", converterName) + write("override val converter : #1T = #1T", itemConverter) write("override val partitionKey: #T = #T(#S)", MapperTypes.Items.keySpec(partitionKeyProp.keySpec), partitionKeyProp.keySpecType, partitionKeyProp.name) if (sortKeyProp != null) { write("override val sortKey: #T = #T(#S)", MapperTypes.Items.keySpec(sortKeyProp.keySpec), sortKeyProp.keySpecType, sortKeyProp.name) 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 34f99ab6eca..b376421c1f0 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 @@ -357,4 +357,51 @@ class SchemaGeneratorPluginTest { val testResult = runner.withArguments("test").build() assertContains(setOf(TaskOutcome.SUCCESS, TaskOutcome.UP_TO_DATE), testResult.task(":test")?.outcome) } + + @Test + fun testDynamoDbIgnore() { + createClassFile("IgnoredProperty") + + 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/IgnoredPropertySchema.kt") + assertTrue(schemaFile.exists()) + + val schemaContents = schemaFile.readText() + + assertContains(schemaContents, "public class IgnoredProperty") + assertContains(schemaContents, "public var id: Int? = null") + assertContains(schemaContents, "public var givenName: String? = null") + assertContains(schemaContents, "public var surname: String? = null") + assertContains(schemaContents, "public var age: Int? = null") + assertContains(schemaContents, "public fun build(): IgnoredProperty") + + // ssn is annotated with DynamoDbIgnore + assertFalse(schemaContents.contains("public var ssn: String? = null")) + } + + @Test + fun testDynamoDbItemConverter() { + createClassFile("custom-item-converter/CustomUser") + createClassFile("custom-item-converter/CustomItemConverter", "src/main/kotlin/my/custom/item/converter") + + 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/CustomUserSchema.kt") + assertTrue(schemaFile.exists()) + + val schemaContents = schemaFile.readText() + assertFalse(schemaContents.contains("public object CustomUserItemConverter : ItemConverter by SimpleItemConverter")) + assertContains( + schemaContents, + """ + public object CustomUserSchema : ItemSchema.PartitionKey { + override val converter : MyCustomUserConverter = MyCustomUserConverter + override val partitionKey: KeySpec = aws.sdk.kotlin.hll.dynamodbmapper.items.KeySpec.Number("id") + } + """.trimIndent(), + ) + } } diff --git a/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/IgnoredProperty.kt b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/IgnoredProperty.kt new file mode 100644 index 00000000000..ff9c091258a --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/IgnoredProperty.kt @@ -0,0 +1,17 @@ +package org.example + +import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbAttribute +import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbIgnore +import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbItem +import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbPartitionKey + +@DynamoDbItem +public data class IgnoredProperty( + @DynamoDbPartitionKey var id: Int, + @DynamoDbAttribute("fName") var givenName: String, + @DynamoDbAttribute("lName") var surname: String, + var age: Int, + + @DynamoDbIgnore + var ssn: String? = null, +) diff --git a/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/custom-item-converter/CustomItemConverter.kt b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/custom-item-converter/CustomItemConverter.kt new file mode 100644 index 00000000000..6c9f12cde5d --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/custom-item-converter/CustomItemConverter.kt @@ -0,0 +1,39 @@ +package my.custom.item.converter + +import aws.sdk.kotlin.hll.dynamodbmapper.items.AttributeDescriptor +import aws.sdk.kotlin.hll.dynamodbmapper.items.ItemConverter +import aws.sdk.kotlin.hll.dynamodbmapper.items.SimpleItemConverter +import aws.sdk.kotlin.hll.dynamodbmapper.values.scalars.IntConverter +import aws.sdk.kotlin.hll.dynamodbmapper.values.scalars.StringConverter +import org.example.CustomUser + +public object MyCustomUserConverter : ItemConverter by SimpleItemConverter( + builderFactory = { CustomUser() }, + build = { this }, + descriptors = arrayOf( + AttributeDescriptor( + "id", + CustomUser::id, + CustomUser::id::set, + IntConverter, + ), + AttributeDescriptor( + "myCustomFirstName", + CustomUser::givenName, + CustomUser::givenName::set, + StringConverter, + ), + AttributeDescriptor( + "myCustomLastName", + CustomUser::surname, + CustomUser::surname::set, + StringConverter, + ), + AttributeDescriptor( + "myCustomAge", + CustomUser::age, + CustomUser::age::set, + IntConverter, + ), + ), +) diff --git a/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/custom-item-converter/CustomUser.kt b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/custom-item-converter/CustomUser.kt new file mode 100644 index 00000000000..75fe3234cdb --- /dev/null +++ b/hll/dynamodb-mapper/dynamodb-mapper-schema-generator-plugin/src/test/resources/custom-item-converter/CustomUser.kt @@ -0,0 +1,12 @@ +package org.example + +import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbItem +import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbPartitionKey + +@DynamoDbItem("my.custom.item.converter.MyCustomUserConverter") +public data class CustomUser( + @DynamoDbPartitionKey var id: Int = 1, + var givenName: String = "Johnny", + var surname: String = "Appleseed", + var age: Int = 0, +)