Skip to content

Commit

Permalink
add support for Mapper initialization; extract interfaces from public…
Browse files Browse the repository at this point in the history
… classes and hide implementations; add KDocs for more types
  • Loading branch information
ianbotsf committed Feb 28, 2024
1 parent a07ff66 commit c7f1d00
Show file tree
Hide file tree
Showing 18 changed files with 683 additions and 376 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/
package aws.sdk.kotlin.hll.dynamodbmapper.processor

import aws.sdk.kotlin.hll.dynamodbmapper.DynamodDbAttribute
import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbAttribute
import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbItem
import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbPartitionKey
import com.google.devtools.ksp.KspExperimental
Expand Down Expand Up @@ -60,7 +60,6 @@ public class MapperProcessor(private val env: SymbolProcessorEnvironment) : Symb
|
|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
|
Expand All @@ -79,7 +78,7 @@ public class MapperProcessor(private val env: SymbolProcessorEnvironment) : Symb
|
|public object $schemaName : ItemSchema.PartitionKey<$className, ${keyProp.typeName.getShortName()}> {
| override val converter: $converterName = $converterName
| override val partitionKey: KeySpec.${keyProp.keySpecType} = ${generateKeySpec(keyProp)}
| 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)
Expand Down Expand Up @@ -179,15 +178,15 @@ private data class Property(val name: String, val ddbName: String, val typeName:
?.let { typeName ->
val isPk = ksProperty.isAnnotationPresent(DynamoDbPartitionKey::class)
val name = ksProperty.simpleName.getShortName()
val ddbName = ksProperty.getAnnotationsByType(DynamodDbAttribute::class).singleOrNull()?.name ?: name
val ddbName = ksProperty.getAnnotationsByType(DynamoDbAttribute::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"
"kotlin.Int" -> "Number"
"kotlin.String" -> "String"
else -> error("Unsupported key type $fqTypeName, expected Int or String")
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
public abstract interface annotation class aws/sdk/kotlin/hll/dynamodbmapper/DynamoDbAttribute : java/lang/annotation/Annotation {
public abstract fun name ()Ljava/lang/String;
}

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

Expand All @@ -7,7 +11,3 @@ public abstract interface annotation class aws/sdk/kotlin/hll/dynamodbmapper/Dyn
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;
}

Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ package aws.sdk.kotlin.hll.dynamodbmapper
* included then the attribute name matches the property name.
*/
@Target(AnnotationTarget.PROPERTY)
public annotation class DynamodDbAttribute(val name: String)
public annotation class DynamoDbAttribute(val name: String)

/**
* Specifies that this class/interface describes an item type in a table. All properties of this type will be mapped to
Expand Down
255 changes: 87 additions & 168 deletions hll/ddb-mapper/dynamodb-mapper/api/dynamodb-mapper.api

Large diffs are not rendered by default.

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

import aws.sdk.kotlin.hll.dynamodbmapper.schemas.ItemSchema
import aws.sdk.kotlin.hll.dynamodbmapper.items.ItemSchema
import aws.sdk.kotlin.hll.dynamodbmapper.pipeline.Interceptor
import aws.sdk.kotlin.services.dynamodb.DynamoDbClient

// TODO refactor to interface, add support for multi-table operations, document, add unit tests
public class DynamoDbMapper(public val client: DynamoDbClient) {
public fun <I, PK> getTable(
/**
* A high-level client for DynamoDB which maps custom data types into DynamoDB attributes and vice versa.
*/
public interface DynamoDbMapper {
public companion object {
/**
* Instantiate a new [DynamoDbMapper]
* @param client The low-level DynamoDB client to use for underlying calls to the service
* @param config A DSL configuration block
*/
public operator fun invoke(client: DynamoDbClient, config: Config.Builder.() -> Unit = { }): DynamoDbMapper =
DynamoDbMapperImpl(client, Config(config))
}

/**
* The low-level DynamoDB client used for underlying calls to the service
*/
public val client: DynamoDbClient

/**
* Get a [Table] reference for performing table operations
* @param T The type of objects which will be read from and/or written to this table
* @param PK The type of the partition key property, either [String], [Number], or [ByteArray]
* @param name The name of the table
* @param schema The [ItemSchema] which describes the table, its keys, and how items are converted
*/
public fun <T, PK> getTable(
name: String,
schema: ItemSchema.PartitionKey<T, PK>,
): Table.PartitionKey<T, PK>

/**
* Get a [Table] reference for performing table operations
* @param T The type of objects which will be read from and/or written to this table
* @param PK The type of the partition key property, either [String], [Number], or [ByteArray]
* @param SK The type of the sort key property, either [String], [Number], or [ByteArray]
* @param name The name of the table
* @param schema The [ItemSchema] which describes the table, its keys, and how items are converted
*/
public fun <T, PK, SK> getTable(
name: String,
schema: ItemSchema.CompositeKey<T, PK, SK>,
): Table.CompositeKey<T, PK, SK>

// TODO add multi-table operations like batchGetItem, transactWriteItems, etc.

/**
* The immutable configuration for a [DynamoDbMapper] instance
*/
public interface Config {
public companion object {
/**
* Instantiate a new [Config] object
* @param config A DSL block for setting properties of the config
*/
public operator fun invoke(config: Builder.() -> Unit = { }): Config =
Builder().apply(config).build()
}

/**
* A list of [Interceptor] instances which will be applied to operations as they move through the request
* pipeline.
*/
public val interceptors: List<Interceptor<*, *, *, *, *>>

/**
* Convert this immutable configuration into a mutable [Builder] object. Updates made to the mutable builder
* properties will not affect this instance.
*/
public fun toBuilder(): Builder

/**
* A mutable configuration builder for a [DynamoDbMapper] instance
*/
public interface Builder {
public companion object {
/**
* Instantiate a new [Builder] object
*/
public operator fun invoke(): Builder = MapperConfigBuilderImpl()
}

/**
* A list of [Interceptor] instances which will be applied to operations as they move through the request
* pipeline.
*/
public var interceptors: MutableList<Interceptor<*, *, *, *, *>>

/**
* Builds this mutable [Builder] object into an immutable [Config] object. Changes made to this instance do
* not affect the built instance.
*/
public fun build(): Config
}
}
}

private data class DynamoDbMapperImpl(
override val client: DynamoDbClient,
private val config: DynamoDbMapper.Config,
) : DynamoDbMapper {
override fun <T, PK> getTable(
name: String,
schema: ItemSchema.PartitionKey<I, PK>,
): Table.PartitionKey<I, PK> = Table(client, name, schema)
schema: ItemSchema.PartitionKey<T, PK>,
): Table.PartitionKey<T, PK> = TableImpl.PartitionKeyImpl(this, name, schema)

public fun <I, PK, SK> getTable(
override fun <T, PK, SK> getTable(
name: String,
schema: ItemSchema.CompositeKey<I, PK, SK>,
): Table.CompositeKey<I, PK, SK> = Table(client, name, schema)
schema: ItemSchema.CompositeKey<T, PK, SK>,
): Table.CompositeKey<T, PK, SK> = TableImpl.CompositeKeyImpl(this, name, schema)
}

private class MapperConfigImpl(override val interceptors: List<Interceptor<*, *, *, *, *>>) : DynamoDbMapper.Config {
override fun toBuilder(): DynamoDbMapper.Config.Builder = DynamoDbMapper.Config.Builder().also {
it.interceptors = interceptors.toMutableList()
}
}

private class MapperConfigBuilderImpl : DynamoDbMapper.Config.Builder {
override var interceptors: MutableList<Interceptor<*, *, *, *, *>> = mutableListOf()

override fun build(): DynamoDbMapper.Config = MapperConfigImpl(interceptors.toList())
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,90 +4,124 @@
*/
package aws.sdk.kotlin.hll.dynamodbmapper

import aws.sdk.kotlin.hll.dynamodbmapper.items.ItemSchema
import aws.sdk.kotlin.hll.dynamodbmapper.model.Item
import aws.sdk.kotlin.hll.dynamodbmapper.model.itemOf
import aws.sdk.kotlin.hll.dynamodbmapper.schemas.ItemSchema
import aws.sdk.kotlin.services.dynamodb.DynamoDbClient
import aws.sdk.kotlin.hll.dynamodbmapper.model.toItem
import aws.sdk.kotlin.services.dynamodb.getItem
import aws.sdk.kotlin.services.dynamodb.paginators.items
import aws.sdk.kotlin.services.dynamodb.paginators.scanPaginated
import aws.sdk.kotlin.services.dynamodb.putItem
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map

// TODO refactor to interface, add support for all operations, document, add unit tests
public sealed class Table<I>(public val client: DynamoDbClient, public val name: String) {
public abstract val schema: ItemSchema<I>

public companion object {
public operator fun <I, PK> invoke(
client: DynamoDbClient,
name: String,
schema: ItemSchema.PartitionKey<I, PK>,
): PartitionKey<I, PK> = PartitionKey(client, name, schema)

public operator fun <I, PK, SK> invoke(
client: DynamoDbClient,
name: String,
schema: ItemSchema.CompositeKey<I, PK, SK>,
): CompositeKey<I, PK, SK> = CompositeKey(client, name, schema)
/**
* Represents a table in DynamoDB and an associated item schema. Operations on this table will invoke low-level
* operations after mapping objects to items and vice versa.
* @param T The type of objects which will be read from and/or written to this table
*/
public interface Table<T> {
/**
* The [DynamoDbMapper] which holds the underlying DynamoDB service client used to invoke operations
*/
public val mapper: DynamoDbMapper

/**
* The name of this table
*/
public val name: String

/**
* The [ItemSchema] for this table which describes how to map objects to items and vice versa
*/
public val schema: ItemSchema<T>

// TODO reimplement operations to use pipeline, extension functions where appropriate, docs, etc.

public suspend fun getItem(key: Item): T?

@Suppress("INAPPLICABLE_JVM_NAME")
@JvmName("getItemByKeyObj")
public suspend fun getItem(keyObj: T): T?

public suspend fun putItem(obj: T)

public fun scan(): Flow<T>

/**
* Represents a table whose primary key is a single partition key
* @param T The type of objects which will be read from and/or written to this table
* @param PK The type of the partition key property, either [String], [Number], or [ByteArray]
*/
public interface PartitionKey<T, PK> : Table<T> {
// TODO reimplement operations to use pipeline, extension functions where appropriate, docs, etc.
public suspend fun getItem(partitionKey: PK): T?
}

public fun scan(): Flow<I> {
val resp = client.scanPaginated {
tableName = name
}
return resp.items().map { schema.converter.fromItem(Item(it)) }
/**
* Represents a table whose primary key is a composite of a partition key and a sort key
* @param T The type of objects which will be read from and/or written to this table
* @param PK The type of the partition key property, either [String], [Number], or [ByteArray]
* @param SK The type of the sort key property, either [String], [Number], or [ByteArray]
*/
public interface CompositeKey<T, PK, SK> : Table<T> {
// TODO reimplement operations to use pipeline, extension functions where appropriate, docs, etc.
public suspend fun getItem(partitionKey: PK, sortKey: SK): T?
}
}

internal suspend fun getItem(key: Item): I? {
val resp = client.getItem {
internal abstract class TableImpl<T>(override val mapper: DynamoDbMapper, override val name: String) : Table<T> {
override suspend fun getItem(key: Item): T? {
val resp = mapper.client.getItem {
tableName = name
this.key = key
}
return resp.item?.let { schema.converter.fromItem(Item(it)) }
return resp.item?.toItem()?.let(schema.converter::fromItem)
}

@Suppress("INAPPLICABLE_JVM_NAME")
@JvmName("getItemByKeyItem")
public abstract suspend fun getItem(keyItem: I): I?
override suspend fun putItem(obj: T) {
mapper.client.putItem {
tableName = name
this.item = schema.converter.toItem(obj)
}
}

public suspend fun putItem(item: I) {
client.putItem {
override fun scan(): Flow<T> {
val resp = mapper.client.scanPaginated {
tableName = name
this.item = schema.converter.toItem(item)
}
return resp.items().map { schema.converter.fromItem(it.toItem() ) }
}

public class PartitionKey<I, PK> internal constructor(
client: DynamoDbClient,
internal class PartitionKeyImpl<T, PK> internal constructor(
mapper: DynamoDbMapper,
name: String,
override val schema: ItemSchema.PartitionKey<I, PK>,
) : Table<I>(client, name) {
override val schema: ItemSchema.PartitionKey<T, PK>,
) : TableImpl<T>(mapper, name), Table.PartitionKey<T, PK> {
private val keyAttributeNames = setOf(schema.partitionKey.name)

@Suppress("INAPPLICABLE_JVM_NAME")
@JvmName("getItemByKeyItem")
override suspend fun getItem(keyItem: I): I? =
getItem(schema.converter.toItem(keyItem, keyAttributeNames))
override suspend fun getItem(keyObj: T): T? =
getItem(schema.converter.toItem(keyObj, keyAttributeNames))

public suspend fun getItem(partitionKey: PK): I? =
override suspend fun getItem(partitionKey: PK): T? =
getItem(itemOf(schema.partitionKey.toField(partitionKey)))
}

public class CompositeKey<I, PK, SK> internal constructor(
client: DynamoDbClient,
internal class CompositeKeyImpl<T, PK, SK> internal constructor(
mapper: DynamoDbMapper,
name: String,
override val schema: ItemSchema.CompositeKey<I, PK, SK>,
) : Table<I>(client, name) {
override val schema: ItemSchema.CompositeKey<T, PK, SK>,
) : TableImpl<T>(mapper, name), Table.CompositeKey<T, PK, SK> {
private val keyAttributeNames = setOf(schema.partitionKey.name, schema.sortKey.name)

@Suppress("INAPPLICABLE_JVM_NAME")
@JvmName("getItemByKeyItem")
override suspend fun getItem(keyItem: I): I? =
getItem(schema.converter.toItem(keyItem, keyAttributeNames))
override suspend fun getItem(keyObj: T): T? =
getItem(schema.converter.toItem(keyObj, keyAttributeNames))

public suspend fun getItem(partitionKey: PK, sortKey: SK): I? =
override suspend fun getItem(partitionKey: PK, sortKey: SK): T? =
getItem(itemOf(schema.partitionKey.toField(partitionKey), schema.sortKey.toField(sortKey)))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ public object DocumentConverter : ItemConverter<Document> {
.mapValues { (_, attr) -> fromAv(attr) }
.let(Document::Map)

override fun toItem(obj: Document, onlyKeys: Set<String>?): Item {
override fun toItem(obj: Document, onlyAttributes: Set<String>?): Item {
require(obj is Document.Map)

val map = if (onlyKeys == null) {
val map = if (onlyAttributes == null) {
obj
} else {
obj.filterKeys { it in onlyKeys }
obj.filterKeys { it in onlyAttributes }
}

return map.mapValues { (_, value) -> toAv(value) }.toItem()
Expand Down
Loading

0 comments on commit c7f1d00

Please sign in to comment.