Skip to content

Commit

Permalink
initial poc commit of DynamoDB Mapper (#1232)
Browse files Browse the repository at this point in the history
  • Loading branch information
ianbotsf authored Feb 27, 2024
1 parent d16f9f7 commit a07ff66
Show file tree
Hide file tree
Showing 30 changed files with 1,301 additions and 0 deletions.
2 changes: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@ configureNexus()
val lintPaths = listOf(
"**/*.{kt,kts}",
"!**/generated-src/**",
"!**/generated/ksp/**",
"!**/kspCaches/**",
"!**/smithyprojections/**",
)

Expand Down
5 changes: 5 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
[versions]
kotlin-version = "1.9.21"
ksp-version = "1.9.21-1.0.16" # Keep in sync with kotlin-version

dokka-version = "1.9.10"

aws-kotlin-repo-tools-version = "0.4.0"
Expand Down Expand Up @@ -43,6 +45,8 @@ kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-c
kotlinx-coroutines-jdk8 = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8", version.ref = "coroutines-version" }
kotlinx-coroutines-slf4j = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-slf4j", version.ref = "coroutines-version" }

ksp-api = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp-version" }

slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j-version" }
slf4j-simple = { module = "org.slf4j:slf4j-simple", version.ref = "slf4j-version" }

Expand Down Expand Up @@ -131,5 +135,6 @@ kotlin-multiplatform = {id = "org.jetbrains.kotlin.multiplatform", version.ref =
kotlinx-benchmark = { id = "org.jetbrains.kotlinx.benchmark", version.ref = "kotlinx-benchmark-version" }
kotlinx-binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version = "0.13.2" }
kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin-version"}
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp-version" }
aws-kotlin-repo-tools-kmp = { id = "aws.sdk.kotlin.gradle.kmp", version.ref = "aws-kotlin-repo-tools-version" }
aws-kotlin-repo-tools-smithybuild = { id = "aws.sdk.kotlin.gradle.smithybuild", version.ref = "aws-kotlin-repo-tools-version" }
91 changes: 91 additions & 0 deletions hll/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

import aws.sdk.kotlin.gradle.dsl.configurePublishing
import aws.sdk.kotlin.gradle.kmp.*
import org.jetbrains.kotlin.gradle.dsl.JvmTarget

description = "High-level libraries for the AWS SDK for Kotlin"

// FIXME 🔽🔽🔽 This is all copied from :aws-runtime and should be commonized 🔽🔽🔽

plugins {
alias(libs.plugins.dokka)
alias(libs.plugins.kotlinx.binary.compatibility.validator)
alias(libs.plugins.aws.kotlin.repo.tools.kmp) apply false
jacoco
}

val sdkVersion: String by project

// capture locally - scope issue with custom KMP plugin
val libraries = libs

subprojects {
if (!needsKmpConfigured) return@subprojects

group = "aws.sdk.kotlin"
version = sdkVersion

apply {
plugin("org.jetbrains.kotlin.multiplatform")
plugin("org.jetbrains.dokka")
plugin(libraries.plugins.aws.kotlin.repo.tools.kmp.get().pluginId)
}

configurePublishing("aws-sdk-kotlin")

kotlin {
explicitApi()

sourceSets {
// dependencies available for all subprojects

named("commonTest") {
dependencies {
implementation(libraries.kotest.assertions.core)
}
}

named("jvmTest") {
dependencies {
implementation(libraries.kotest.assertions.core.jvm)
implementation(libraries.slf4j.simple)
}
}
}
}

kotlin.sourceSets.all {
// Allow subprojects to use internal APIs
// See https://kotlinlang.org/docs/reference/opt-in-requirements.html#opting-in-to-using-api
listOf("kotlin.RequiresOptIn").forEach { languageSettings.optIn(it) }
}

dependencies {
dokkaPlugin(project(":dokka-aws"))
}

tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_1_8)
freeCompilerArgs.add("-Xexpect-actual-classes")
}
}
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinNativeCompile> {
compilerOptions {
freeCompilerArgs.add("-Xexpect-actual-classes")
}
}
}

apiValidation {
val availableSubprojects = subprojects.map { it.name }.toSet()

ignoredProjects += listOf(
"dynamodb-mapper-annotation-processor",
"ddb-mapper-annotation-processor-test",
).filter { it in availableSubprojects } // Some projects may not be in the build depending on bootstrapping
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

kotlin {
sourceSets {
jvmMain {
dependencies {
implementation(project(":hll:ddb-mapper:dynamodb-mapper-annotations"))
implementation(libs.ksp.api)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
aws.sdk.kotlin.hll.dynamodbmapper.processor.MapperProcessorProvider
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package aws.sdk.kotlin.hll.dynamodbmapper.processor

import aws.sdk.kotlin.hll.dynamodbmapper.DynamodDbAttribute
import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbItem
import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbPartitionKey
import com.google.devtools.ksp.KspExperimental
import com.google.devtools.ksp.getAnnotationsByType
import com.google.devtools.ksp.isAnnotationPresent
import com.google.devtools.ksp.processing.*
import com.google.devtools.ksp.symbol.*
import com.google.devtools.ksp.validate

private val annotationName = DynamoDbItem::class.qualifiedName!!

public class MapperProcessor(private val env: SymbolProcessorEnvironment) : SymbolProcessor {
override fun process(resolver: Resolver): List<KSAnnotated> {
env.logger.info("Searching for symbols annotated with $annotationName")
val annotated = resolver.getSymbolsWithAnnotation(annotationName)
val invalid = annotated.filterNot { it.validate() }.toList()
env.logger.info("Found invalid classes $invalid")

annotated
.toList()
.also { env.logger.info("Found annotated classes: $it") }
.filterIsInstance<KSClassDeclaration>()
.filter { it.validate() }
.forEach { it.accept(ItemVisitor(), Unit) }

return invalid
}

private inner class ItemVisitor : KSVisitorVoid() {
override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) {
val basePackageName = classDeclaration.packageName.asString()
val packageName = "$basePackageName.mapper.schemas"

val className = classDeclaration.qualifiedName!!.getShortName()
val builderName = "${className}Builder"
val converterName = "${className}Converter"
val schemaName = "${className}Schema"

val props = classDeclaration.getAllProperties().mapNotNull(Property.Companion::from)
val keyProp = checkNotNull(props.singleOrNull { it.isPk }) {
"Expected exactly one @DynamoDbPartitionKey annotation on a property"
}

env.codeGenerator.createNewFile(
Dependencies(true, classDeclaration.containingFile!!),
packageName,
schemaName,
).use { file ->
file.bufferedWriter().use { writer ->
writer.append(
"""
|package $packageName
|
|import aws.sdk.kotlin.hll.dynamodbmapper.*
|import aws.sdk.kotlin.hll.dynamodbmapper.items.*
|import aws.sdk.kotlin.hll.dynamodbmapper.schemas.*
|import aws.sdk.kotlin.hll.dynamodbmapper.values.*
|import $basePackageName.$className
|
|public class $builderName {
${generateProperties(props)}
| ${generateBuildMethod(className, props)}
|}
|
|public object $converterName : ItemConverter<$className> by SimpleItemConverter(
| builderFactory = ::$builderName,
| build = $builderName::build,
| descriptors = listOf(
${generateDescriptors(className, builderName, props)}
| ),
|)
|
|public object $schemaName : ItemSchema.PartitionKey<$className, ${keyProp.typeName.getShortName()}> {
| override val converter: $converterName = $converterName
| override val partitionKey: KeySpec.${keyProp.keySpecType} = ${generateKeySpec(keyProp)}
|}
|
|public fun DynamoDbMapper.get${className}Table(name: String): Table.PartitionKey<$className, ${keyProp.typeName.getShortName()}> = getTable(name, $schemaName)
|
""".trimMargin(),
)
}
}
}

private fun generateBuildMethod(className: String, props: Sequence<Property>) =
buildString {
appendLine("public fun build(): $className {")

props.forEach { prop ->
appendLine(""" val ${prop.name} = requireNotNull(${prop.name}) { "Missing value for $className.${prop.name}" }""")
}

appendLine()

append(" return $className(")
append(props.joinToString(", ") { it.name })
appendLine(")")

appendLine(" }")
}.trimEnd()

private fun generateDescriptors(
className: String,
builderName: String,
props: Sequence<Property>,
) = buildString {
props.forEach { prop ->
val converterType = when (val fqTypeName = prop.typeName.asString()) {
"aws.smithy.kotlin.runtime.time.Instant" -> "InstantConverter.Default"
"kotlin.Boolean" -> "BooleanConverter"
"kotlin.Int" -> "IntConverter"
"kotlin.String" -> "StringConverter"
else -> error("Unsupported attribute type $fqTypeName")
}

append("| AttributeDescriptor(")

// key
append("\"")
append(prop.ddbName)
append("\", ")

// getter
append(className)
append("::")
append(prop.name)
append(", ")

// setter
append(builderName)
append("::")
append(prop.name)
append("::set, ")

// converter
append(converterType)

appendLine("),")
}
}.trimEnd()

private fun generateKeySpec(keyProp: Property) = buildString {
append("KeySpec.")
append(keyProp.keySpecType)
append("(\"")
append(keyProp.name)
append("\")")
}

private fun generateProperties(props: Sequence<Property>) = buildString {
props.forEach { prop ->
append("| public var ")
append(prop.name)
append(": ")
append(prop.typeName.asString())
appendLine("? = null")
}
}
}
}

private data class Property(val name: String, val ddbName: String, val typeName: KSName, val isPk: Boolean) {
companion object {
@OptIn(KspExperimental::class)
fun from(ksProperty: KSPropertyDeclaration) = ksProperty
.getter
?.returnType
?.resolve()
?.declaration
?.qualifiedName
?.let { typeName ->
val isPk = ksProperty.isAnnotationPresent(DynamoDbPartitionKey::class)
val name = ksProperty.simpleName.getShortName()
val ddbName = ksProperty.getAnnotationsByType(DynamodDbAttribute::class).singleOrNull()?.name ?: name
Property(name, ddbName, typeName, isPk)
}
}
}

private val Property.keySpecType: String
get() = when (val fqTypeName = typeName.asString()) {
"kotlin.Int" -> "N"
"kotlin.String" -> "S"
else -> error("Unsupported key type $fqTypeName, expected Int or String")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package aws.sdk.kotlin.hll.dynamodbmapper.processor

import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
import com.google.devtools.ksp.processing.SymbolProcessorProvider

public class MapperProcessorProvider : SymbolProcessorProvider {
override fun create(environment: SymbolProcessorEnvironment): MapperProcessor = MapperProcessor(environment)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
public abstract interface annotation class aws/sdk/kotlin/hll/dynamodbmapper/DynamoDbItem : java/lang/annotation/Annotation {
}

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

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

public abstract interface annotation class aws/sdk/kotlin/hll/dynamodbmapper/DynamodDbAttribute : java/lang/annotation/Annotation {
public abstract fun name ()Ljava/lang/String;
}

4 changes: 4 additions & 0 deletions hll/ddb-mapper/dynamodb-mapper-annotations/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
Loading

0 comments on commit a07ff66

Please sign in to comment.