Skip to content

Commit

Permalink
Support for Bean Table Extensions (#13)
Browse files Browse the repository at this point in the history
🏗️ ✨ use the built-in StaticImmutableTableSchema and support extensions
  • Loading branch information
oharaandrew314 authored Nov 20, 2023
1 parent 1222538 commit 0e4c188
Show file tree
Hide file tree
Showing 13 changed files with 245 additions and 448 deletions.
17 changes: 17 additions & 0 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,23 @@ data class Appointment(
)
```

## Support for Extensions

Bean table extensions are supported. Register the extension to the client like normal,
and then add the `DynamoDbBean` extension annotation to the getter of the property.

```kotlin
data class Post(
@DynamoKtPartitionKey
val id: UUID,

val title: String,

@get:DynamoDbVersionAttribute // add extension annotation to the getter (ie get:<annotation>)
val version: Int
)
```

## Samples

See the [Samples](/src/test/kotlin/io/andrewohara/dynamokt/samples)
10 changes: 4 additions & 6 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,23 +1,21 @@
plugins {
kotlin("jvm") version "1.9.10"
kotlin("jvm") version "1.9.20"
id("jacoco")
id("maven-publish")
}

repositories {
mavenCentral()
maven { url = uri("https://jitpack.io") }
}

dependencies {
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation(kotlin("reflect"))

api("software.amazon.awssdk:dynamodb-enhanced:2.20.86+")
api("software.amazon.awssdk:dynamodb-enhanced:2.21.26+")

testImplementation(kotlin("test"))
testImplementation("io.kotest:kotest-assertions-core-jvm:5.6.2")
testImplementation("org.http4k:http4k-aws:5.8.0.0")
testImplementation("org.http4k:http4k-connect-amazon-dynamodb-fake:5.2.0.0")
testImplementation("org.http4k:http4k-connect-amazon-dynamodb-fake:5.5.0.1")
}

tasks.test {
Expand Down
69 changes: 0 additions & 69 deletions src/main/kotlin/io/andrewohara/dynamokt/DataClassAttribute.kt

This file was deleted.

54 changes: 0 additions & 54 deletions src/main/kotlin/io/andrewohara/dynamokt/DataClassTableMetadata.kt

This file was deleted.

60 changes: 13 additions & 47 deletions src/main/kotlin/io/andrewohara/dynamokt/DataClassTableSchema.kt
Original file line number Diff line number Diff line change
@@ -1,56 +1,22 @@
package io.andrewohara.dynamokt

import software.amazon.awssdk.enhanced.dynamodb.EnhancedType
import software.amazon.awssdk.enhanced.dynamodb.TableSchema
import software.amazon.awssdk.services.dynamodb.model.AttributeValue
import software.amazon.awssdk.services.dynamodb.model.ResourceNotFoundException
import java.lang.IllegalArgumentException
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticImmutableTableSchema
import kotlin.reflect.KClass
import kotlin.reflect.full.*
import kotlin.reflect.full.declaredMemberProperties
import kotlin.reflect.full.primaryConstructor

class DataClassTableSchema<Item: Any>(dataClass: KClass<Item>): TableSchema<Item> {

init {
require(dataClass.isData) { "$dataClass must be a data class" }
}
private val metadata = DataClassTableMetadata(dataClass)
private val type = EnhancedType.documentOf(dataClass.java, this)
private val constructor = requireNotNull(dataClass.primaryConstructor) { "$dataClass must have a primary constructor"}
private val attributes = DataClassAttribute.create(dataClass).associateBy { it.attributeName }
fun <Item: Any> DataClassTableSchema(dataClass: KClass<Item>): TableSchema<Item> {
val props = dataClass.declaredMemberProperties.sortedBy { it.name }
val params = dataClass.primaryConstructor!!.parameters.sortedBy { it.name }

override fun mapToItem(attributeMap: Map<String, AttributeValue>): Item {
val arguments = attributes.values
.mapNotNull { attr -> attributes[attr.attributeName]?.unConvert(attributeMap) }
.toMap()
require(dataClass.isData) { "$dataClass must be a data class" }
require(props.size == params.size) { "$dataClass properties MUST all be declared in the constructor" }
val constructor = requireNotNull(dataClass.primaryConstructor) { "$dataClass must have a primary constructor"}

return try {
constructor.callBy(arguments)
} catch (e: Throwable) {
throw IllegalArgumentException("Could not map item to ${type.rawClass().simpleName}", e)
}
}

override fun itemToMap(item: Item, ignoreNulls: Boolean): Map<String, AttributeValue> {
return attributes.values
.associate { attr -> attr.convert(item) }
.filterValues { it.nul() != true || !ignoreNulls }
}

override fun itemToMap(item: Item, attributes: Collection<String>): Map<String, AttributeValue> {
return this.attributes.values
.filter { it.attributeName in attributes }
.associate { attr -> attr.convert(item) }
}

override fun attributeValue(item: Item, attributeName: String): AttributeValue? {
return attributes[attributeName]?.convert(item)?.second
}

override fun tableMetadata() = metadata

override fun itemType(): EnhancedType<Item> = type

override fun attributeNames() = attributes.keys.toList()

override fun isAbstract() = false
return StaticImmutableTableSchema.builder(dataClass.java, ImmutableDataClassBuilder::class.java)
.newItemBuilder({ ImmutableDataClassBuilder(constructor) }, { it.build() as Item })
.attributes(dataClass.declaredMemberProperties.map { it.toImmutableDataClassAttribute(dataClass) })
.build()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package io.andrewohara.dynamokt

import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter
import software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider
import software.amazon.awssdk.enhanced.dynamodb.EnhancedType
import software.amazon.awssdk.enhanced.dynamodb.mapper.ImmutableAttribute
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTag
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.BeanTableSchemaAttributeTag
import kotlin.reflect.KClass
import kotlin.reflect.KProperty1
import kotlin.reflect.KType
import kotlin.reflect.KVisibility
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.staticFunctions
import kotlin.reflect.jvm.javaType

private fun KType.toEnhancedType(): EnhancedType<out Any> {
return when(val clazz = classifier as KClass<Any>) {
List::class -> {
val listType = arguments.first().type!!.toEnhancedType()
EnhancedType.listOf(listType)
}
Set::class -> {
val setType = arguments.first().type!!.toEnhancedType()
EnhancedType.setOf(setType)
}
Map::class -> {
val (key, value) = arguments.map { it.type!!.toEnhancedType() }
EnhancedType.mapOf(key, value)
}
else -> {
if (clazz.isData) {
EnhancedType.documentOf(clazz.java, DataClassTableSchema(clazz))
} else {
EnhancedType.of(javaType)
}
}
}
}

private fun <Attr: Any?> initConverter(clazz: KClass<out AttributeConverter<Attr>>): AttributeConverter<Attr> {
clazz.constructors.firstOrNull { it.visibility == KVisibility.PUBLIC }
?.let { return it.call() }

return clazz.staticFunctions
.filter { it.name == "create" }
.filter { it.visibility == KVisibility.PUBLIC }
.first { it.parameters.isEmpty() }
.call() as AttributeConverter<Attr>
}

private fun KProperty1<out Any, *>.tags() = buildList {
for (annotation in annotations) {
when(annotation) {
is DynamoKtPartitionKey -> add(StaticAttributeTags.primaryPartitionKey())
is DynamoKtSortKey -> add(StaticAttributeTags.primarySortKey())
is DynamoKtSecondaryPartitionKey -> add(StaticAttributeTags.secondaryPartitionKey(annotation.indexNames.toList()))
is DynamoKtSecondarySortKey -> add(StaticAttributeTags.secondarySortKey(annotation.indexNames.toList()))
}
}

// add extension tags
for (annotation in getter.annotations) {
val tagAnnotation = annotation.annotationClass.findAnnotation<BeanTableSchemaAttributeTag>() ?: continue
val generator = tagAnnotation.value.staticFunctions.find { it.name == "attributeTagFor" }
?: error("static attributeTagFor function required for ${tagAnnotation::class.simpleName}")

add(generator.call(annotation) as StaticAttributeTag)
}
}

internal fun <Table: Any, Attr: Any?> KProperty1<Table, Attr>.toImmutableDataClassAttribute(dataClass: KClass<Table>): ImmutableAttribute<Table, ImmutableDataClassBuilder, Attr> {
val converter = findAnnotation<DynamoKtConverted>()
?.converter
?.let { it as KClass<AttributeConverter<Attr>> }
?.let { initConverter(it) }
?: AttributeConverterProvider.defaultProvider().converterFor(returnType.toEnhancedType())

val dynamoName = findAnnotation<DynamoKtAttribute>()?.name?: name

return ImmutableAttribute
.builder(EnhancedType.of(dataClass.java), EnhancedType.of(ImmutableDataClassBuilder::class.java), returnType.toEnhancedType() as EnhancedType<Attr>)
.name(dynamoName)
.getter(::get)
.setter { builder, value -> builder[name] = value }
.attributeConverter(converter as AttributeConverter<Attr>)
.tags(tags())
.build()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.andrewohara.dynamokt

import java.lang.IllegalArgumentException
import kotlin.reflect.KFunction

internal class ImmutableDataClassBuilder(private val constructor: KFunction<Any>) {
private val values = mutableMapOf<String, Any?>()

fun build(): Any {
val params = constructor.parameters.associateWith { values[it.name] }
.mapNotNull { (param, value) -> if (value == null && param.isOptional) null else param to value } // filter out null optional constructor values
.toMap()

return try {
constructor.callBy(params)
} catch (e: Throwable) {
throw IllegalArgumentException("Could not map item to ${constructor.returnType}", e)
}
}

operator fun get(name: String) = values[name]
operator fun set(name: String, value: Any?) = values.set(name, value)
}
Loading

0 comments on commit 0e4c188

Please sign in to comment.