Skip to content
Open
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
Expand Up @@ -2,6 +2,10 @@ 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/DynamoDbAttributeConverter : java/lang/annotation/Annotation {
public abstract fun converter ()Ljava/lang/Class;
}

public abstract interface annotation class aws/sdk/kotlin/hll/dynamodbmapper/DynamoDbIgnore : java/lang/annotation/Annotation {
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,14 @@
description = "DynamoDbMapper annotations"
extra["displayName"] = "AWS :: SDK :: Kotlin :: HLL :: DynamoDbMapper :: Annotations"
extra["moduleName"] = "aws.sdk.kotlin.hll.dynamodbmapper.annotations"

kotlin {
sourceSets {
commonMain {
dependencies {
// For ValueConverter
implementation(project(":hll:dynamodb-mapper:dynamodb-mapper"))
}
}
}
}
Comment on lines +10 to +19
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment: I'd hoped to keep the dependencies clean for the annotations package but we obviously need the ValueConverter (and soon, ItemConverter) types. I wonder if we should extract a dynamodb-mapper-core-api package which contains some important interfaces but no implementations.

Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,28 @@
*/
package aws.sdk.kotlin.hll.dynamodbmapper

import aws.sdk.kotlin.hll.dynamodbmapper.values.ValueConverter
import kotlin.reflect.KClass

/**
* Specifies the attribute name for a property in a [DynamoDbItem]-annotated class/interface. If this annotation is not
* included then the attribute name matches the property name.
*/
@Target(AnnotationTarget.PROPERTY)
public annotation class DynamoDbAttribute(val name: String)

/**
* Specifies the type of [ValueConverter] to be used when processing this attribute.
*/
public annotation class DynamoDbAttributeConverter(val converter: KClass<out ValueConverter<*>>)

Comment on lines +17 to +21
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment: I always seem to forget that annotation classes can only take KClass/Class arguments, not actual class instances. 😖 Several of the value converters (e.g., BooleanValueConverter, the various number value converters, etc.) are defined as val instances. We'll need to update those class definitions (or maybe object?) to allow users to select them in annotations. Can you either leave a FIXME somewhere and/or create a second task to handle updating the converter declarations?

/**
* 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<ItemConverter>, which will require splitting codegen modules due to a circular dependency
// FIXME Update to take a KClass<ItemConverter>?
@Target(AnnotationTarget.CLASS)
public annotation class DynamoDbItem(val converterName: String = "")
Comment on lines -20 to 30
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment: Yes, we'll definitely still want to do this. I think that'll be far more convenient.


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ dependencies {
implementation(project(":hll:hll-codegen"))
implementation(project(":hll:dynamodb-mapper:dynamodb-mapper-annotations"))
implementation(project(":hll:dynamodb-mapper:dynamodb-mapper-codegen"))
implementation(project(":hll:dynamodb-mapper:dynamodb-mapper")) // for ValueConverter.kt

testImplementation(libs.junit.jupiter)
testImplementation(libs.junit.jupiter.params)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ internal class HighLevelRenderer(
attributes,
)

val annotation = SchemaRenderer(annotated, renderCtx)
val annotation = SchemaRenderer(logger, annotated, renderCtx)
annotation.render()
Comment on lines -45 to 46
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Isn't the logger already available in renderCtx?

}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/
package aws.sdk.kotlin.hll.dynamodbmapper.codegen.annotations.rendering

import aws.sdk.kotlin.hll.codegen.core.ImportDirective
import aws.sdk.kotlin.hll.codegen.model.*
import aws.sdk.kotlin.hll.codegen.rendering.BuilderRenderer
import aws.sdk.kotlin.hll.codegen.rendering.RenderContext
Expand All @@ -18,6 +19,7 @@ import com.google.devtools.ksp.KspExperimental
import com.google.devtools.ksp.getAnnotationsByType
import com.google.devtools.ksp.getConstructors
import com.google.devtools.ksp.isAnnotationPresent
import com.google.devtools.ksp.processing.KSPLogger
import com.google.devtools.ksp.symbol.*

/**
Expand All @@ -27,6 +29,7 @@ import com.google.devtools.ksp.symbol.*
*/
@OptIn(KspExperimental::class)
internal class SchemaRenderer(
private val logger: KSPLogger,
private val classDeclaration: KSClassDeclaration,
private val ctx: RenderContext,
) : RendererBase(ctx, "${classDeclaration.qualifiedName!!.getShortName()}Schema") {
Expand Down Expand Up @@ -148,6 +151,7 @@ internal class SchemaRenderer(
}

private fun renderAttributeDescriptor(prop: KSPropertyDeclaration) {
logger.info("Rendering an attribute descriptor for ${prop.simpleName.asString()}")
withBlock("#T(", "),", MapperTypes.Items.AttributeDescriptor) {
write("#S,", prop.ddbName) // key
write("#L,", "$className::${prop.name}") // getter
Expand All @@ -160,8 +164,25 @@ internal class SchemaRenderer(
}

// converter
renderValueConverter(prop.type.resolve())
write(",")
// KSP requires extra work to get a class argument out of an annotation, can't just use getAnnotationsByType
// https://slack-chats.kotlinlang.org/t/8480301/hello-again-how-do-you-get-a-kclass-out-from-an-annotation-a
val attributeValueConverterFqn = prop.annotations
.singleOrNull { it.annotationType.resolve().declaration.qualifiedName?.asString() == DynamoDbAttributeConverter::class.qualifiedName }
?.arguments
?.single()
?.value
?.let { it as? KSType }
?.declaration
?.qualifiedName
?.asString()

attributeValueConverterFqn?.let {
imports += ImportDirective(it)
write("$it(),")
} ?: run {
renderValueConverter(prop.type.resolve())
write(",")
}
}
}

Expand Down Expand Up @@ -229,7 +250,6 @@ internal class SchemaRenderer(
Types.Kotlin.UInt -> MapperTypes.Values.Scalars.UIntValueConverter
Types.Kotlin.UShort -> MapperTypes.Values.Scalars.UShortValueConverter
Types.Kotlin.ULong -> MapperTypes.Values.Scalars.ULongValueConverter

else -> error("Unsupported attribute type $type")
},
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ dependencies {
implementation(libs.ksp.gradle.plugin)

implementation(project(":hll:hll-codegen")) // for RenderOptions
implementation(project(":hll:dynamodb-mapper:dynamodb-mapper")) // for ValueConverter
implementation(project(":hll:dynamodb-mapper:dynamodb-mapper-schema-codegen")) // for AnnotationsProcessorOptions
implementation(libs.smithy.kotlin.runtime.core) // for AttributeKey

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -569,4 +569,41 @@ class SchemaGeneratorPluginTest {
""".trimIndent(),
)
}

@Test
fun testDynamoDbAttributeConverter() {
createClassFile("attribute-converter/Employee")
createClassFile("attribute-converter/HealthcareConverter")

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/EmployeeSchema.kt")
assertTrue(schemaFile.exists())

val schemaContents = schemaFile.readText()

assertContains(schemaContents, "import org.example.OccupationConverter")
assertContains(
schemaContents,
""" AttributeDescriptor(
"occupation",
Employee::occupation,
Employee::occupation::set,
org.example.OccupationConverter(),
),""",
)

// Test cross-package converter
assertContains(schemaContents, "import a.different.pkg.HealthcareConverter")
assertContains(
schemaContents,
""" AttributeDescriptor(
"healthcare",
Employee::healthcare,
Employee::healthcare::set,
a.different.pkg.HealthcareConverter(),
),""",
Comment on lines +587 to +606
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: String indentation is a little wonky.

)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package org.example

import a.different.pkg.HealthcareConverter
import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbAttributeConverter
import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbItem
import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbPartitionKey
import aws.sdk.kotlin.hll.dynamodbmapper.values.ValueConverter
import aws.sdk.kotlin.hll.mapping.core.converters.MonoConverter
import aws.sdk.kotlin.services.dynamodb.model.AttributeValue

@DynamoDbItem
data class Employee(
@DynamoDbPartitionKey
var id: Int = 1,
var givenName: String = "Johnny",
var surname: String = "Appleseed",

@property:DynamoDbAttributeConverter(OccupationConverter::class)
var occupation: Occupation = Occupation("Student", 0),

@property:DynamoDbAttributeConverter(HealthcareConverter::class)
var healthcare: Healthcare = Healthcare(false),
Comment on lines +23 to +27
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Having to qualify the target in the annotation line seems kind of ugly and unintuitive. Is this still required if the annotation class is annotated with @Target(AnnotationTarget.PROPERTY)?

)

data class Occupation(val title: String, val salary: Int)
data class Healthcare(val enrolled: Boolean)

class OccupationConverter : ValueConverter<Occupation> {
override val right = MonoConverter<Occupation, AttributeValue> { AttributeValue.S(it.title + "#" + it.salary) }

override val left = MonoConverter<AttributeValue, Occupation> {
val content = it.asS()
val (title, salary) = content.split("#")
Occupation(title, salary.toInt())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package a.different.pkg

import aws.sdk.kotlin.hll.dynamodbmapper.values.ValueConverter
import aws.sdk.kotlin.hll.mapping.core.converters.MonoConverter
import aws.sdk.kotlin.services.dynamodb.model.AttributeValue
import org.example.Healthcare

class HealthcareConverter : ValueConverter<Healthcare> {
override val right = MonoConverter<Healthcare, AttributeValue> { AttributeValue.S(it.enrolled.toString()) }

override val left = MonoConverter<AttributeValue, Healthcare> {
val content = it.asS()
val enrolled = (content == "true")
Healthcare(enrolled)
}
}
Loading