Skip to content

Commit 472d760

Browse files
#11 ✨ support recursive table structure
1 parent d70a83e commit 472d760

File tree

3 files changed

+58
-14
lines changed

3 files changed

+58
-14
lines changed
Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,45 @@
11
package io.andrewohara.dynamokt
22

33
import software.amazon.awssdk.enhanced.dynamodb.TableSchema
4+
import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.MetaTableSchemaCache
45
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticImmutableTableSchema
56
import kotlin.reflect.KClass
67
import kotlin.reflect.KVisibility
78
import kotlin.reflect.full.declaredMemberProperties
89
import kotlin.reflect.full.primaryConstructor
910

10-
1111
fun <Item: Any> DataClassTableSchema(dataClass: KClass<Item>): TableSchema<Item> {
12-
val props = dataClass.declaredMemberProperties.sortedBy { it.name }
13-
val params = dataClass.primaryConstructor!!.parameters.sortedBy { it.name }
12+
require(dataClass.isData) { "$dataClass must be a data class" }
13+
return dataClassTableSchema(dataClass, MetaTableSchemaCache())
14+
}
15+
16+
internal fun <Item: Any> dataClassTableSchema(dataClass: KClass<Item>, schemaCache: MetaTableSchemaCache): TableSchema<Item> {
17+
val props = dataClass.declaredMemberProperties
18+
require(props.size == dataClass.primaryConstructor!!.parameters.size) {
19+
"${dataClass.simpleName} properties MUST all be declared in the constructor"
20+
}
21+
1422
val constructor = dataClass.primaryConstructor
1523
?.takeIf { it.visibility == KVisibility.PUBLIC }
1624
?: error("${dataClass.simpleName} must have a public primary constructor")
1725

18-
require(dataClass.isData) { "$dataClass must be a data class" }
19-
require(props.size == params.size) { "${dataClass.simpleName} properties MUST all be declared in the constructor" }
26+
val metaSchema = schemaCache.getOrCreate(dataClass.java)
2027

2128
return StaticImmutableTableSchema.builder(dataClass.java, ImmutableDataClassBuilder::class.java)
2229
.newItemBuilder({ ImmutableDataClassBuilder(constructor) }, { it.build() as Item })
23-
.attributes(dataClass.declaredMemberProperties.map { it.toImmutableDataClassAttribute(dataClass) })
30+
.attributes(props.map { it.toImmutableDataClassAttribute(dataClass, schemaCache) })
2431
.build()
32+
.also { metaSchema.initialize(it) }
33+
}
34+
35+
internal fun <Item: Any> recursiveDataClassTableSchema(dataClass: KClass<Item>, schemaCache: MetaTableSchemaCache): TableSchema<Item> {
36+
val metaTableSchema = schemaCache.get(dataClass.java)
37+
38+
if (metaTableSchema.isPresent) {
39+
return if (metaTableSchema.get().isInitialized) {
40+
metaTableSchema.get().concreteTableSchema()
41+
} else metaTableSchema.get()
42+
}
43+
44+
return dataClassTableSchema(dataClass, schemaCache)
2545
}

src/main/kotlin/io/andrewohara/dynamokt/ImmutableDataClassAttribute.kt

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package io.andrewohara.dynamokt
33
import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter
44
import software.amazon.awssdk.enhanced.dynamodb.AttributeConverterProvider
55
import software.amazon.awssdk.enhanced.dynamodb.EnhancedType
6+
import software.amazon.awssdk.enhanced.dynamodb.internal.mapper.MetaTableSchemaCache
67
import software.amazon.awssdk.enhanced.dynamodb.mapper.ImmutableAttribute
78
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTag
89
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticAttributeTags
@@ -15,23 +16,23 @@ import kotlin.reflect.full.findAnnotation
1516
import kotlin.reflect.full.staticFunctions
1617
import kotlin.reflect.jvm.javaType
1718

18-
private fun KType.toEnhancedType(): EnhancedType<out Any> {
19+
private fun KType.toEnhancedType(schemaCache: MetaTableSchemaCache): EnhancedType<out Any> {
1920
return when(val clazz = classifier as KClass<Any>) {
2021
List::class -> {
21-
val listType = arguments.first().type!!.toEnhancedType()
22+
val listType = arguments.first().type!!.toEnhancedType(schemaCache)
2223
EnhancedType.listOf(listType)
2324
}
2425
Set::class -> {
25-
val setType = arguments.first().type!!.toEnhancedType()
26+
val setType = arguments.first().type!!.toEnhancedType(schemaCache)
2627
EnhancedType.setOf(setType)
2728
}
2829
Map::class -> {
29-
val (key, value) = arguments.map { it.type!!.toEnhancedType() }
30+
val (key, value) = arguments.map { it.type!!.toEnhancedType(schemaCache) }
3031
EnhancedType.mapOf(key, value)
3132
}
3233
else -> {
3334
if (clazz.isData) {
34-
EnhancedType.documentOf(clazz.java, DataClassTableSchema(clazz))
35+
EnhancedType.documentOf(clazz.java, recursiveDataClassTableSchema(clazz, schemaCache))
3536
} else {
3637
EnhancedType.of(javaType)
3738
}
@@ -70,17 +71,24 @@ private fun KProperty1<out Any, *>.tags() = buildList {
7071
}
7172
}
7273

73-
internal fun <Table: Any, Attr: Any?> KProperty1<Table, Attr>.toImmutableDataClassAttribute(dataClass: KClass<Table>): ImmutableAttribute<Table, ImmutableDataClassBuilder, Attr> {
74+
internal fun <Table: Any, Attr: Any?> KProperty1<Table, Attr>.toImmutableDataClassAttribute(
75+
dataClass: KClass<Table>,
76+
schemaCache: MetaTableSchemaCache
77+
): ImmutableAttribute<Table, ImmutableDataClassBuilder, Attr> {
7478
val converter = findAnnotation<DynamoKtConverted>()
7579
?.converter
7680
?.let { it as KClass<AttributeConverter<Attr>> }
7781
?.let { initConverter(it) }
78-
?: AttributeConverterProvider.defaultProvider().converterFor(returnType.toEnhancedType())
82+
?: AttributeConverterProvider.defaultProvider().converterFor(returnType.toEnhancedType(schemaCache))
7983

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

8286
return ImmutableAttribute
83-
.builder(EnhancedType.of(dataClass.java), EnhancedType.of(ImmutableDataClassBuilder::class.java), returnType.toEnhancedType() as EnhancedType<Attr>)
87+
.builder(
88+
EnhancedType.of(dataClass.java),
89+
EnhancedType.of(ImmutableDataClassBuilder::class.java),
90+
returnType.toEnhancedType(schemaCache) as EnhancedType<Attr>
91+
)
8492
.name(dynamoName)
8593
.getter(::get)
8694
.setter { builder, value -> builder[name] = value }

src/test/kotlin/io/andrewohara/dynamokt/DataClassTableSchemaTest.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder
55
import io.kotest.matchers.shouldBe
66
import org.junit.jupiter.api.Test
77
import software.amazon.awssdk.core.SdkBytes
8+
import software.amazon.awssdk.enhanced.dynamodb.TableSchema
89
import software.amazon.awssdk.services.dynamodb.model.AttributeValue
910
import java.lang.IllegalArgumentException
1011
import java.lang.IllegalStateException
@@ -116,4 +117,19 @@ class DataClassTableSchemaTest {
116117
DataClassTableSchema(Person::class)
117118
}.message shouldBe "Person must have a public primary constructor"
118119
}
120+
121+
@Test
122+
fun `recursive model`() {
123+
data class Item(val id: Int, val inner: Item?)
124+
125+
val item = Item(1, Item(2, null))
126+
127+
DataClassTableSchema(Item::class).itemToMap(item, true) shouldBe mapOf(
128+
"id" to AttributeValue.fromN("1"),
129+
"inner" to AttributeValue.fromM(mapOf(
130+
"id" to AttributeValue.fromN("2"),
131+
"inner" to AttributeValue.fromNul(true)
132+
))
133+
)
134+
}
119135
}

0 commit comments

Comments
 (0)