-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support for Bean Table Extensions (#13)
🏗️ ✨ use the built-in StaticImmutableTableSchema and support extensions
- Loading branch information
1 parent
1222538
commit 0e4c188
Showing
13 changed files
with
245 additions
and
448 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
69 changes: 0 additions & 69 deletions
69
src/main/kotlin/io/andrewohara/dynamokt/DataClassAttribute.kt
This file was deleted.
Oops, something went wrong.
54 changes: 0 additions & 54 deletions
54
src/main/kotlin/io/andrewohara/dynamokt/DataClassTableMetadata.kt
This file was deleted.
Oops, something went wrong.
60 changes: 13 additions & 47 deletions
60
src/main/kotlin/io/andrewohara/dynamokt/DataClassTableSchema.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} |
90 changes: 90 additions & 0 deletions
90
src/main/kotlin/io/andrewohara/dynamokt/ImmutableDataClassAttribute.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} |
23 changes: 23 additions & 0 deletions
23
src/main/kotlin/io/andrewohara/dynamokt/ImmutableDataClassBuilder.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
Oops, something went wrong.