All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.aws.sdk.kotlin.hll.dynamodbmapper.items.HeterogeneousItemConverter.kt Maven / Gradle / Ivy

There is a newer version: 1.3.99-beta
Show newest version
/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */
package aws.sdk.kotlin.hll.dynamodbmapper.items

import aws.sdk.kotlin.hll.dynamodbmapper.model.Item
import aws.sdk.kotlin.hll.dynamodbmapper.model.buildItem
import aws.sdk.kotlin.services.dynamodb.model.AttributeValue
import aws.smithy.kotlin.runtime.ExperimentalApi

/**
 * An item converter which handles heterogeneous (i.e., incongruent) data types by way of a string discriminator
 * attribute identified by [typeAttribute]. The given [typeMapper] function must return a string type name for an object
 * which will be used for the [typeAttribute] attribute. Finally, the given [subConverters] map identifies the delegate
 * converters for each type.
 *
 * This converter is particularly (although not _solely_) useful for mapping polymorphic structures. For example, given
 * a class hierarchy:
 *
 * ```kotlin
 * sealed interface Vehicle
 *
 * @DynamoDbItem
 * data class Car(
 *     @DynamoDbPartitionKey val id: Int,
 *     val manufacturer: String,
 *     val model: String,
 *     val year: Int,
 * ) : Vehicle
 *
 * @DynamoDbItem
 * data class Bike(
 *     @DynamoDbPartitionKey val id: Int,
 *     val manufacturer: String
 *     val gears: Int,
 *     val isElectric: Boolean,
 * ) : Vehicle
 * ```
 *
 * A heterogeneous item converter can be constructed:
 *
 * ```kotlin
 * fun vehicleType(obj: Vehicle) = when (obj) {
 *     is Car -> "car"
 *     is Bike -> "bike"
 * }
 *
 * val vehicleConverter = HeterogeneousItemConverter(
 *     typeMapper = ::vehicleType,
 *     typeAttribute = "type",
 *     subConverters = mapOf(
 *         "car" to CarConverter,
 *         "bike" to BikeConverter,
 *     ),
 * )
 * ```
 *
 * Objects mapped in this manner will use only the attributes relevant to their specific type, plus the [typeAttribute].
 * For example, given the following items and PutItem calls:
 *
 * ```kotlin
 * val vehicles = listOf(
 *     Car(1, "Ford", "Model T", 1928),
 *     Bike(2, "Schwinn", 10, false),
 *     Car(3, "Edsel", "Corsair", 1958),
 *     Bike(4, "Kuwahara", 1, false),
 * )
 *
 * val table = ... // some table which uses the vehicleConverter from above in its schema
 *
 * vehicles.forEach { vehicle ->
 *     table.putItem { item = vehicle }
 * }
 * ```
 *
 * Items would be persisted in the table as:
 *
 * | **id** | **type** | **manufacturer** | **model** | **year** | **gears** | **isElectric** |
 * |-------:|----------|------------------|-----------|---------:|----------:|----------------|
 * |      1 | car      | Ford             | Model T   |     1928 |           |                |
 * |      2 | bike     | Schwinn          |           |          |        10 | false          |
 * |      3 | car      | Edsel            | Corsair   |     1958 |           |                |
 * |      4 | bike     | Kuwahara         |           |          |         1 | false          |
 *
 * @param T The common type ancestor for all subtypes handled by this converter. This may be a base class, interface, or
 * even [Any].
 * @param typeMapper A function which accepts an instance of the common type [T] and returns the string identifier for
 * the type. This identifier is written/read from the attribute identified by [typeAttribute] and used as a lookup key
 * in [subConverters].
 * @param typeAttribute The name of the attribute in which to store/read type information. This attribute will be
 * present for every item persisted via this converter. It should ideally be an attribute which doesn't conflict with
 * other attributes used by subconverters.
 * @param subConverters A map of type names (the same returned by [typeMapper]) to [ItemConverter] instances. If the
 * [typeMapper] function returns a type name which does not exist in this map, or if an item is read containing a type
 * attribute value which does not exist in this map, an exception will be thrown.
 */
@ExperimentalApi
public class HeterogeneousItemConverter(
    public val typeMapper: (T) -> String,
    public val typeAttribute: String,
    public val subConverters: Map>,
) : ItemConverter {
    override fun convertFrom(to: Item): T {
        val attr = to[typeAttribute] ?: error("Missing $typeAttribute")
        val typeValue = attr.asSOrNull() ?: error("No string value for $attr")
        val converter = subConverters[typeValue] ?: error("No converter for $typeValue")
        return converter.convertFrom(to)
    }

    override fun convertTo(from: T, onlyAttributes: Set?): Item {
        val typeValue = typeMapper(from)
        val converter = subConverters[typeValue] ?: error("No converter for $typeValue")

        return buildItem {
            if (onlyAttributes?.contains(typeAttribute) != false) {
                put(typeAttribute, AttributeValue.S(typeValue))
            }

            putAll(converter.convertTo(from, onlyAttributes))
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy