Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
262 changes: 94 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,105 @@
*/
package aws.sdk.kotlin.hll.dynamodbmapper

import aws.sdk.kotlin.hll.dynamodbmapper.schemas.ItemSchema
import aws.sdk.kotlin.hll.dynamodbmapper.internal.DynamoDbMapperImpl
import aws.sdk.kotlin.hll.dynamodbmapper.internal.MapperConfigBuilderImpl
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 =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Any reason to default this to {} instead of nullable?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simply wanting to avoid nullability when a unit function accomplishes the same thing.

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<I, PK>,
): Table.PartitionKey<I, PK> = Table(client, name, schema)
schema: ItemSchema.PartitionKey<T, PK>,
): Table.PartitionKey<T, PK>

public fun <I, PK, SK> getTable(
/**
* 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<I, PK, SK>,
): Table.CompositeKey<I, PK, SK> = Table(client, name, schema)
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<*, *, *, *, *>>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Will the types here become more specific than * or is this the expected type?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll take a second look at this when I'm implementing the request pipeline but my gut feeling is that this is the expected type. Each of these type params will vary based on the high-level object type (T) and the operation type (HReq, LReq, LRes, HRes). We could possibly make a common base type for all high-level request/response types but I think at most it'd be a marker interface. The low-level request/response types have no common supertype and we can't enforce one for the high-level object type either.


/**
* 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
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,90 +4,61 @@
*/
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.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>
/**
* 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> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Should this be a sealed interface?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could be sealed but that might complicate users' unit testing. My goal is to maintain maximum flexibility in the public API by leaving as many types as possible/sensible open interfaces which can be mocked without a library.

Right now I don't have a use case which requires knowing all the concrete implementations of Table (e.g., for when matching). If one arises we may revisit this.

/**
* The [DynamoDbMapper] which holds the underlying DynamoDB service client used to invoke operations
*/
public val mapper: DynamoDbMapper

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)
/**
* The name of this table
*/
public val name: String

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)
}
/**
* The [ItemSchema] for this table which describes how to map objects to items and vice versa
*/
public val schema: ItemSchema<T>

public fun scan(): Flow<I> {
val resp = client.scanPaginated {
tableName = name
}
return resp.items().map { schema.converter.fromItem(Item(it)) }
}
// TODO reimplement operations to use pipeline, extension functions where appropriate, docs, etc.

internal suspend fun getItem(key: Item): I? {
val resp = client.getItem {
tableName = name
this.key = key
}
return resp.item?.let { schema.converter.fromItem(Item(it)) }
}
public suspend fun getItem(key: Item): T?

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

public suspend fun putItem(item: I) {
client.putItem {
tableName = name
this.item = schema.converter.toItem(item)
}
}

public class PartitionKey<I, PK> internal constructor(
client: DynamoDbClient,
name: String,
override val schema: ItemSchema.PartitionKey<I, PK>,
) : Table<I>(client, name) {
private val keyAttributeNames = setOf(schema.partitionKey.name)
public suspend fun putItem(obj: T)

@Suppress("INAPPLICABLE_JVM_NAME")
@JvmName("getItemByKeyItem")
override suspend fun getItem(keyItem: I): I? =
getItem(schema.converter.toItem(keyItem, keyAttributeNames))
public fun scan(): Flow<T>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: How will this take into account filtering, exclusive start key, etc?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for not clarifying more in the PR description but all the operation methods below the // TODO are out of scope for this PR. I rearranged them slightly as part of extracting the interface from the implementation but none of these are final.

The eventual answer to your question will be that each high-level operation will have a canonical method that accepts a high-level request object and returns a high-level response object. Then there will be convenience methods for a handful of APIs which deal in more primitive types. The eventual scan operation will definitely be able to specify filters, exclusive start keys, etc.


public suspend fun getItem(partitionKey: PK): I? =
getItem(itemOf(schema.partitionKey.toField(partitionKey)))
/**
* 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 class CompositeKey<I, PK, SK> internal constructor(
client: DynamoDbClient,
name: String,
override val schema: ItemSchema.CompositeKey<I, PK, SK>,
) : Table<I>(client, name) {
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))

public suspend fun getItem(partitionKey: PK, sortKey: SK): I? =
getItem(itemOf(schema.partitionKey.toField(partitionKey), schema.sortKey.toField(sortKey)))
/**
* 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?
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package aws.sdk.kotlin.hll.dynamodbmapper.internal

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

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

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

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

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

override fun build() = MapperConfigImpl(interceptors.toList())
}
Loading