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