From 6b0f6a18b89995dc6f7caa4caed6e5298e73f2df Mon Sep 17 00:00:00 2001 From: Kaloyan Karaivanov Date: Fri, 14 Jun 2024 12:31:44 +0300 Subject: [PATCH] Introduce enumSerializer for type-safe enum handling Introduce enumSerializer function toprovide a more type-safe and concise way to serialize enums. This new approach supports nullable enums (EnumClass?) and eliminates the need for custom enum serializers. Deprecate FirstOrdinalSerializer in favor of enumSerializer, as it offers better type safety and nullability handling. Update usage to reflect the newrecommended approach. This change improves the robustness and maintainability of enum serialization within the project. --- .../generativeai/common/util/serialization.kt | 147 ++++++++++++++++++ 1 file changed, 147 insertions(+) diff --git a/common/src/main/kotlin/com/google/ai/client/generativeai/common/util/serialization.kt b/common/src/main/kotlin/com/google/ai/client/generativeai/common/util/serialization.kt index 65040487..37456c29 100644 --- a/common/src/main/kotlin/com/google/ai/client/generativeai/common/util/serialization.kt +++ b/common/src/main/kotlin/com/google/ai/client/generativeai/common/util/serialization.kt @@ -21,6 +21,8 @@ import com.google.ai.client.generativeai.common.SerializationException import kotlin.reflect.KClass import kotlinx.serialization.KSerializer import kotlinx.serialization.SerialName +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.buildClassSerialDescriptor import kotlinx.serialization.encoding.Decoder @@ -34,6 +36,14 @@ import kotlinx.serialization.encoding.Encoder * When an unknown enum value is found, the enum itself will be logged to stderr with a message * about opening an issue on GitHub regarding the new enum value. */ +@Deprecated( + level = DeprecationLevel.WARNING, + message = "This class is deprecated. Use enumSerializer() with nullability EnumClass? type instead. Not throw exception with serialization", + replaceWith = ReplaceWith( + expression = "enumSerializer()", + imports = ["com.google.ai.client.generativeai.common.util.enumSerializer"] + ), +) class FirstOrdinalSerializer>(private val enumClass: KClass) : KSerializer { override val descriptor: SerialDescriptor = buildClassSerialDescriptor("FirstOrdinalSerializer") @@ -81,3 +91,140 @@ val > T.serialName: String */ fun > KClass.enumValues(): Array = java.enumConstants ?: throw SerializationException("$simpleName is not a valid enum type.") + +/** + * A generic serializer for enum classes using Kotlin Serialization with caches. + * + * This serializer handles the serialization and deserialization of enum values as strings, + * using either the `serialName` (if available) or the regular `name` of the enum. + * + * @param T The enum type to serialize. + */ + +inline fun > enumSerializer() = object : KSerializer { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("EnumSerializer", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: T?) { + (value?.serialName ?: value?.name)?.let { encoder.encodeString(it) } + } + + override fun deserialize(decoder: Decoder): T? { + val decodeString = decoder.decodeString() + return decodeString.enumBySerialName() as T? + ?: decodeString.enumByName() as T? + ?: Log.e( + "serializer", + """ + |Unknown enum value found: "$decodeString" in ${T::class.simpleName} + |This usually means the backend was updated, and the SDK needs to be updated to match it. + |Check if there's a new version for the SDK, otherwise please open an issue on our + |GitHub to bring it to our attention: + |https://github.com/google/google-ai-android + """.trimMargin(), + ).run { null } //todo T::class.java.enumConstants?.firstOrNull() + } +} + +/** + * A utility object that provides caching for enum name and serialized name lookups. + * + * This object maintains three caches:* + * - `serialNameByEnum`: Maps enum instances to their serialized names (as defined by the `@SerialName` annotation). + * - `enumByEnumName`: Maps enum names to their corresponding enum instances. + * - `enumBySerialName`: Maps serialized names to their corresponding enum instances. + * + * The caches are populated lazily, meaning that the mappings are generated only when a particular enum class is accessed for the first time. + */ + +object Caches { + private val serialNameByEnum: MutableMap, Map, String>> = mutableMapOf() + private val enumByEnumName: MutableMap, Map>> = mutableMapOf() + private val enumBySerialName: MutableMap, Map>> = mutableMapOf() + + /** + * Populates the cachesfor the given enum class. + * + * @param declaringClass The enum class to generate caches for. + */ + private fun > makeCache(declaringClass: Class) { + val mapNames = declaringClass.enumConstants!! + val pairs: List> = mapNames + .mapNotNull { constant -> + val serialName = constant + .declaringJavaClass + .getField(constant.name) + .getAnnotation(SerialName::class.java)?.value + serialName?.let { constant to it } + } + serialNameByEnum[declaringClass] = pairs.toMap() + enumByEnumName[declaringClass] = mapNames.associateBy { it.name } + enumBySerialName[declaringClass] = pairs.associate { it.second to it.first } + } + + /** + * Returns the serialized name of the given enum instance. + * + * @param enum The enum instance to get the serialized name for. + * @return The serialized name of the enum, or `null` if not found. + */ + + fun > serialNameByEnum(enum: Enum): String? { + val declaringClass: Class = enum.declaringJavaClass + serialNameByEnum[declaringClass] ?: makeCache(declaringClass) + return serialNameByEnum[declaringClass]!![enum] + } + + /** + * Returns the enum instance corresponding to the given enum name. + * + * @param declaringClass The enum class to search within. + * @param serialName The simple name of the enum to find. + * @return The enum instance, or `null` if not found. + */ + + fun > enumByName(declaringClass: Class, serialName: String): Enum<*>? { + enumByEnumName[declaringClass] ?: makeCache(declaringClass) + return enumByEnumName[declaringClass]!![serialName] + } + + /** + * Returns the enum instance corresponding to the given serialized name. + * + * @param declaringClass The enum class to search within. + * @param serialName The serialized name of the enum to find. + * @return The enum instance, or `null` if not found. + */ + + fun > enumBySerialName(declaringClass: Class, serialName: String): Enum<*>? { + enumBySerialName[declaringClass] ?: makeCache(declaringClass) + return enumBySerialName[declaringClass]!![serialName] + } +} + +/** + * Returns the serialized name of the enum instance, as defined by the `@SerialName` annotation. + * + * @returnThe serialized name of the enum, or `null` if no `@SerialName` annotation is present. + */ + +val > Enum.serialName: String? + get() = Caches.serialNameByEnum(this) + +/** + * Attempts to findan enum instance of the reified type [T] by its simple name. + * + * @return The enum instance corresponding to the given name, or `null` if not found. + */ + +inline fun > String.enumByName(): Enum<*>? = + Caches.enumByName(T::class.java, this) + +/** + * Attempts to find an enum instance of the reified type [T] by its serialized name. + * + * @return The enum instance corresponding to the given serialized name, or `null` if not found. + */ + +inline fun > String.enumBySerialName(): Enum<*>? = + Caches.enumBySerialName(T::class.java, this) \ No newline at end of file