Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add attribute converters for "standard" values #1381

Merged
merged 6 commits into from
Aug 27, 2024
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
5 changes: 3 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ slf4j-version = "2.0.9"
[libraries]
aws-kotlin-repo-tools-build-support = { module="aws.sdk.kotlin.gradle:build-support", version.ref = "aws-kotlin-repo-tools-version" }

kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin-version"}
kotlin-stdlib-jdk8 = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin-version"}
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin-version" }
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin-version" }
kotlin-stdlib-jdk8 = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin-version" }
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin-version" }
kotlin-test-junit5 = { module = "org.jetbrains.kotlin:kotlin-test-junit5", version.ref = "kotlin-version" }
dokka-core = { module = "org.jetbrains.dokka:dokka-core", version.ref = "dokka-version" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,10 @@ class SchemaRenderer(

private val AnnotatedClassProperty.valueConverter: Type
get() = when (typeName.asString()) {
"aws.smithy.kotlin.runtime.time.Instant" -> MapperTypes.Values.DefaultInstantConverter
"kotlin.Boolean" -> MapperTypes.Values.BooleanConverter
"kotlin.Int" -> MapperTypes.Values.IntConverter
"kotlin.String" -> MapperTypes.Values.StringConverter
"aws.smithy.kotlin.runtime.time.Instant" -> MapperTypes.Values.SmithyTypes.DefaultInstantConverter
"kotlin.Boolean" -> MapperTypes.Values.Scalars.BooleanConverter
"kotlin.Int" -> MapperTypes.Values.Scalars.IntConverter
"kotlin.String" -> MapperTypes.Values.Scalars.StringConverter
// TODO Add additional "standard" item converters
else -> error("Unsupported attribute type ${typeName.asString()}")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,15 @@ object MapperTypes {
}

object Values {
val DefaultInstantConverter = TypeRef(Pkg.Hl.Values, "InstantConverter.Default")
val BooleanConverter = TypeRef(Pkg.Hl.Values, "BooleanConverter")
val IntConverter = TypeRef(Pkg.Hl.Values, "IntConverter")
val StringConverter = TypeRef(Pkg.Hl.Values, "StringConverter")
object Scalars {
val BooleanConverter = TypeRef(Pkg.Hl.ScalarValues, "BooleanConverter")
val IntConverter = TypeRef(Pkg.Hl.ScalarValues, "IntConverter")
val StringConverter = TypeRef(Pkg.Hl.ScalarValues, "StringConverter")
}

object SmithyTypes {
val DefaultInstantConverter = TypeRef(Pkg.Hl.SmithyTypeValues, "InstantConverters.Default")
}
}

object PipelineImpl {
Expand Down
164 changes: 120 additions & 44 deletions hll/dynamodb-mapper/dynamodb-mapper/api/dynamodb-mapper.api

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions hll/dynamodb-mapper/dynamodb-mapper/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,14 @@ kotlin {
commonMain {
dependencies {
api(project(":services:dynamodb"))
api(project(":hll:hll-mapping-core"))
api(libs.kotlinx.coroutines.core)
}
}

commonTest {
dependencies {
implementation(libs.kotlin.reflect)
implementation(libs.kotlinx.coroutines.test)
implementation(libs.kotest.assertions.core)
implementation(libs.kotest.runner.junit5)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ import aws.smithy.kotlin.runtime.InternalApi
import aws.smithy.kotlin.runtime.content.Document
import aws.smithy.kotlin.runtime.util.toNumber

// FIXME Combine with DocumentValueConverter or refactor to commonize as much code as possible
public object DocumentConverter : ItemConverter<Document> {
override fun fromItem(item: Item): Document = item
.mapValues { (_, attr) -> fromAv(attr) }
.mapValues { (_, attr) -> fromAttributeValue(attr) }
.let(Document::Map)

override fun toItem(obj: Document, onlyAttributes: Set<String>?): Item {
Expand All @@ -25,26 +26,26 @@ public object DocumentConverter : ItemConverter<Document> {
obj.filterKeys { it in onlyAttributes }
}

return map.mapValues { (_, value) -> toAv(value) }.toItem()
return map.mapValues { (_, value) -> toAttributeValue(value) }.toItem()
}
}

@OptIn(InternalApi::class)
private fun fromAv(attr: AttributeValue): Document? = when (attr) {
private fun fromAttributeValue(attr: AttributeValue): Document? = when (attr) {
is AttributeValue.Null -> null
is AttributeValue.N -> Document.Number(attr.value.toNumber()!!) // FIXME need better toNumber logic
is AttributeValue.S -> Document.String(attr.value)
is AttributeValue.Bool -> Document.Boolean(attr.value)
is AttributeValue.L -> Document.List(attr.value.map(::fromAv))
is AttributeValue.M -> Document.Map(attr.value.mapValues { (_, nestedValue) -> fromAv(nestedValue) })
is AttributeValue.L -> Document.List(attr.value.map(::fromAttributeValue))
is AttributeValue.M -> Document.Map(attr.value.mapValues { (_, nestedValue) -> fromAttributeValue(nestedValue) })
else -> error("Documents do not support ${attr::class.qualifiedName}")
}

private fun toAv(value: Document?): AttributeValue = when (value) {
private fun toAttributeValue(value: Document?): AttributeValue = when (value) {
null -> AttributeValue.Null(true)
is Document.Number -> AttributeValue.N(value.value.toString())
is Document.String -> AttributeValue.S(value.value)
is Document.Boolean -> AttributeValue.Bool(value.value)
is Document.List -> AttributeValue.L(value.value.map(::toAv))
is Document.Map -> AttributeValue.M(value.mapValues { (_, nestedValue) -> toAv(nestedValue) })
is Document.List -> AttributeValue.L(value.value.map(::toAttributeValue))
is Document.Map -> AttributeValue.M(value.mapValues { (_, nestedValue) -> toAttributeValue(nestedValue) })
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ package aws.sdk.kotlin.hll.dynamodbmapper.items
import aws.sdk.kotlin.hll.dynamodbmapper.model.Item

/**
* Defines the logic for converting between objects and items
* Defines the logic for converting between objects and DynamoDB items
* @param T The type of objects which will be converted
*/
public interface ItemConverter<T> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,16 @@ public class SimpleItemConverter<T, B>(
*
* ```kotlin
* val descriptor = descriptors[name] // AttributeDescriptor<*, T, B>
* val value = descriptor.converter.fromAv(av) // Any?
* val value = descriptor.converter.fromAttributeValue(av) // Any?
* descriptor.setter(builder, value) // Type mismatch for value. Required: Nothing, Found: Any?
* ```
*/
fun <A> AttributeDescriptor<A, T, B>.fromAv(av: AttributeValue) =
builder.setter(converter.fromAv(av))
fun <A> AttributeDescriptor<A, T, B>.fromAttributeValue(attr: AttributeValue) =
builder.setter(converter.convertFrom(attr))

item.forEach { (name, av) ->
item.forEach { (name, attr) ->
// TODO make behavior for unknown attributes configurable (ignore, exception, other?)
descriptors[name]?.fromAv(av)
descriptors[name]?.fromAttributeValue(attr)
}

return builder.build()
Expand All @@ -66,11 +66,11 @@ public class SimpleItemConverter<T, B>(
* ```kotlin
* val descriptor = descriptors[name] // AttributeDescriptor<*, T, B>
* val value = descriptor.getter(obj) // Any?
* descriptor.converter.toAv(value) // Type mismatch for value. Required: Nothing, Found: Any?
* descriptor.converter.toAttributeValue(value) // Type mismatch for value. Required: Nothing, Found: Any?
* ```
*/
fun <A> AttributeDescriptor<A, T, B>.toAv() =
converter.toAv(getter(obj))
fun <A> AttributeDescriptor<A, T, B>.toAttributeValue() =
converter.convertTo(getter(obj))

val descriptors = if (onlyAttributes == null) {
this.descriptors.values
Expand All @@ -79,7 +79,7 @@ public class SimpleItemConverter<T, B>(
}

return buildItem {
descriptors.forEach { desc -> put(desc.name, desc.toAv()) }
descriptors.forEach { desc -> put(desc.name, desc.toAttributeValue()) }
}
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package aws.sdk.kotlin.hll.dynamodbmapper.values

import aws.sdk.kotlin.hll.mapping.core.converters.Converter
import aws.sdk.kotlin.hll.mapping.core.converters.SplittingConverter
import aws.sdk.kotlin.hll.mapping.core.converters.mergeBy
import aws.sdk.kotlin.hll.mapping.core.util.Either
import aws.sdk.kotlin.services.dynamodb.model.AttributeValue
import kotlin.reflect.KClass

/**
* Converts between potentially `null` values and
* [DynamoDB `NULL` values](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.Null).
* Note that this class is a [SplittingConverter] and the logic for handling non-null values is undefined in this class.
* Thus, it is typically used in conjunction with the [NullableConverter] factory function or via [mergeBy].
* @param V The non-nullable type
*/
public class NullableConverter<V : Any>(klass: KClass<V>) : SplittingConverter<V?, V, AttributeValue, AttributeValue> {
override fun convertTo(from: V?): Either<AttributeValue, V> = when (from) {
null -> Either.Left(AttributeValue.Null(true))
else -> Either.Right(from)
}

override fun convertFrom(to: AttributeValue): Either<V?, AttributeValue> = when (to) {
is AttributeValue.Null -> Either.Left(null)
else -> Either.Right(to)
}
}

/**
* Initializes a new [NullableConverter] for the given reified type [V]
*/
public inline fun <reified V : Any> NullableConverter(): NullableConverter<V> = NullableConverter(V::class)

@Suppress("ktlint:standard:function-naming")
public inline fun <reified F : Any> NullableConverter(
delegate: Converter<F, AttributeValue>,
): Converter<F?, AttributeValue> = NullableConverter<F>().mergeBy(delegate)

This file was deleted.

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

import aws.sdk.kotlin.hll.mapping.core.converters.Converter
import aws.sdk.kotlin.services.dynamodb.model.AttributeValue

// TODO document, add unit tests
public interface ValueConverter<A> {
public fun fromAv(attr: AttributeValue): A
public fun toAv(value: A): AttributeValue
}
/**
* Defines the logic for converting individual values between a high-level type [V] (e.g., [String], [Boolean], [Map])
* and
* [DynamoDB data types](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes)
* @param V The type of high-level values which will be converted to low-level DynamoDB attribute values
*/
public typealias ValueConverter<V> = Converter<V, AttributeValue>
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package aws.sdk.kotlin.hll.dynamodbmapper.values.collections

import aws.sdk.kotlin.hll.dynamodbmapper.values.ValueConverter
import aws.sdk.kotlin.hll.mapping.core.converters.Converter
import aws.sdk.kotlin.hll.mapping.core.converters.collections.mapFrom
import aws.sdk.kotlin.services.dynamodb.model.AttributeValue

/**
* Converts between [List] and
* [DynamoDB `L` values](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.Document.List).
* Note that the lists must contain already-converted [AttributeValue] elements. This converter is typically chained
* with another converter which handles converting elements to [AttributeValue] either by using the factory function
* [ListConverter] or using the [mapFrom] extension method.
*
* For example:
*
* ```kotlin
* val intListConv = ListConverter(IntConverter) // ValueConverter<List<Int>>
* val intListConv2 = ListConverter.mapFrom(IntConverter) // same as above
* ```
*/
public val ListConverter: ValueConverter<List<AttributeValue>> = Converter(AttributeValue::L, AttributeValue::asL)

/**
* Creates a new list converter using the given [elementConverter] as a delegate
* @param F The type of elements in the list
* @param elementConverter A converter for transforming between values of [F] and [AttributeValue]
*/
@Suppress("ktlint:standard:function-naming")
public fun <F> ListConverter(elementConverter: Converter<F, AttributeValue>): ValueConverter<List<F>> =
ListConverter.mapFrom(elementConverter)
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package aws.sdk.kotlin.hll.dynamodbmapper.values.collections

import aws.sdk.kotlin.hll.dynamodbmapper.values.ValueConverter
import aws.sdk.kotlin.hll.mapping.core.converters.Converter
import aws.sdk.kotlin.hll.mapping.core.converters.collections.mapFrom
import aws.sdk.kotlin.hll.mapping.core.converters.collections.mapKeysFrom
import aws.sdk.kotlin.hll.mapping.core.converters.collections.mapValuesFrom
import aws.sdk.kotlin.services.dynamodb.model.AttributeValue

/**
* Converts between [Map] and
* [DynamoDB `M` values](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes.Document.Map).
* Note that the maps must contain [String] keys and already-converted [AttributeValue] values. This converter is
* typically chained with another converter which handles converting values to [AttributeValue] either by using the
* factory function [MapConverter] or by using the [mapFrom]/[mapValuesFrom]/[mapKeysFrom] extension methods.
*
* ```kotlin
* val instantMapConv = MapConverter(InstantConverter.Default) // ValueConverter<Map<String, Instant>>
* val instantMapConv2 = MapConverter.mapValuesFrom(InstantConverter.Default) // same as above
* ```
*/
public val MapConverter: ValueConverter<Map<String, AttributeValue>> = Converter(AttributeValue::M, AttributeValue::asM)

/**
* Creates a new map converter using the given [keyConverter] and [valueConverter] as delegates
* @param K The type of keys in the map
* @param V The type of values in the map
* @param keyConverter A converter for transforming between [K] keys and [String] keys
* @param valueConverter A converter for transforming between [V] values and [AttributeValue]
*/
@Suppress("ktlint:standard:function-naming")
public fun <K, V> MapConverter(
keyConverter: Converter<K, String>,
valueConverter: ValueConverter<V>,
): ValueConverter<Map<K, V>> = MapConverter.mapFrom(keyConverter, valueConverter)

/**
* Creates a new string-keyed map converter using the given [valueConverter] as a delegate
* @param V The type of values in the map
* @param valueConverter A converter for transforming between [V] values and [AttributeValue]
*/
@Suppress("ktlint:standard:function-naming")
public fun <V> MapConverter(valueConverter: ValueConverter<V>): ValueConverter<Map<String, V>> =
MapConverter.mapValuesFrom(valueConverter)
Loading
Loading