Skip to content

Commit

Permalink
Remove type parameter from Choice (#868)
Browse files Browse the repository at this point in the history
Because Choice was generic, something like the following could happen:

val choice: Choice<String> = Json.decodeFromString(
    Choice.serializer(String.serializer()),
    string = "{\"name\":\"name\",\"value\":1234}",
)
val value: String = choice.value // ClassCastException

Apart from this, the type parameter wasn't really useful since it was
only used in one place.

Therefore, the type parameter was removed from Choice and its usages.
  • Loading branch information
lukellmann authored Sep 17, 2023
1 parent d918920 commit 2dad036
Show file tree
Hide file tree
Showing 9 changed files with 109 additions and 62 deletions.
6 changes: 4 additions & 2 deletions common/api/common.api
Original file line number Diff line number Diff line change
Expand Up @@ -1849,6 +1849,7 @@ public abstract class dev/kord/common/entity/Choice {
}

public final class dev/kord/common/entity/Choice$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
public final fun serializer (Lkotlinx/serialization/KSerializer;)Lkotlinx/serialization/KSerializer;
}

Expand Down Expand Up @@ -2915,11 +2916,11 @@ public final class dev/kord/common/entity/DiscordAutoComplete {
public final fun getChoices ()Ljava/util/List;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
public static final synthetic fun write$Self (Ldev/kord/common/entity/DiscordAutoComplete;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;Lkotlinx/serialization/KSerializer;)V
public static final synthetic fun write$Self (Ldev/kord/common/entity/DiscordAutoComplete;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;)V
}

public final class dev/kord/common/entity/DiscordAutoComplete$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public synthetic fun <init> (Lkotlinx/serialization/KSerializer;)V
public static final field INSTANCE Ldev/kord/common/entity/DiscordAutoComplete$$serializer;
public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ldev/kord/common/entity/DiscordAutoComplete;
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
Expand All @@ -2930,6 +2931,7 @@ public final class dev/kord/common/entity/DiscordAutoComplete$$serializer : kotl
}

public final class dev/kord/common/entity/DiscordAutoComplete$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
public final fun serializer (Lkotlinx/serialization/KSerializer;)Lkotlinx/serialization/KSerializer;
}

Expand Down
100 changes: 60 additions & 40 deletions common/src/commonMain/kotlin/entity/Interactions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.builtins.MapSerializer
import kotlinx.serialization.builtins.nullable
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
import kotlinx.serialization.encoding.*
import kotlinx.serialization.json.*

Expand Down Expand Up @@ -132,7 +133,7 @@ public data class ApplicationCommandOption(
val descriptionLocalizations: Optional<Map<Locale, String>?> = Optional.Missing(),
val default: OptionalBoolean = OptionalBoolean.Missing,
val required: OptionalBoolean = OptionalBoolean.Missing,
val choices: Optional<List<Choice<@Serializable(NotSerializable::class) Any?>>> = Optional.Missing(),
val choices: Optional<List<Choice>> = Optional.Missing(),
val autocomplete: OptionalBoolean = OptionalBoolean.Missing,
val options: Optional<List<ApplicationCommandOption>> = Optional.Missing(),
@SerialName("channel_types")
Expand All @@ -154,6 +155,7 @@ public data class ApplicationCommandOption(
* e.g: `Choice<@Serializable(NotSerializable::class) Any?>`
* The serialization is handled by [Choice] serializer instead where we don't care about the generic type.
*/
@Deprecated("This is no longer used, deprecated without a replacement.", level = DeprecationLevel.WARNING)
@KordExperimental
public object NotSerializable : KSerializer<Any?> {
override fun deserialize(decoder: Decoder): Nothing = error("This operation is not supported.")
Expand All @@ -162,80 +164,88 @@ public object NotSerializable : KSerializer<Any?> {
}


private val LocalizationSerializer =
Optional.serializer(MapSerializer(Locale.serializer(), String.serializer()).nullable)

@Serializable(Choice.Serializer::class)
public sealed class Choice<out T> {
public sealed class Choice {
public abstract val name: String
public abstract val nameLocalizations: Optional<Map<Locale, String>?>
public abstract val value: T
public abstract val value: Any

public data class IntegerChoice(
override val name: String,
override val nameLocalizations: Optional<Map<Locale, String>?>,
override val value: Long,
) : Choice<Long>()
) : Choice()

public data class NumberChoice(
override val name: String,
override val nameLocalizations: Optional<Map<Locale, String>?>,
override val value: Double
) : Choice<Double>()
) : Choice()

public data class StringChoice(
override val name: String,
override val nameLocalizations: Optional<Map<Locale, String>?>,
override val value: String
) : Choice<String>()
) : Choice()

internal object Serializer : KSerializer<Choice<*>> {
internal object Serializer : KSerializer<Choice> {
private val localizationsSerializer = MapSerializer(Locale.serializer(), String.serializer()).nullable

override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Choice") {
element<String>("name")
element<JsonPrimitive>("value")
element<Map<Locale, String>?>("name_localizations", isOptional = true)
override val descriptor = buildClassSerialDescriptor("dev.kord.common.entity.Choice") {
element("name", String.serializer().descriptor)
element("value", JsonPrimitive.serializer().descriptor)
element("name_localizations", localizationsSerializer.descriptor, isOptional = true)
}

override fun deserialize(decoder: Decoder) = decoder.decodeStructure(descriptor) {
override fun serialize(encoder: Encoder, value: Choice) = encoder.encodeStructure(descriptor) {
encodeStringElement(descriptor, index = 0, value.name)
when (value) {
is IntegerChoice -> encodeLongElement(descriptor, index = 1, value.value)
is NumberChoice -> encodeDoubleElement(descriptor, index = 1, value.value)
is StringChoice -> encodeStringElement(descriptor, index = 1, value.value)
}
if (value.nameLocalizations !is Optional.Missing) {
encodeSerializableElement(descriptor, index = 2, localizationsSerializer, value.nameLocalizations.value)
}
}

lateinit var name: String
override fun deserialize(decoder: Decoder) = decoder.decodeStructure(descriptor) {
var name: String? = null
var nameLocalizations: Optional<Map<Locale, String>?> = Optional.Missing()
lateinit var value: JsonPrimitive
var value: JsonPrimitive? = null

while (true) {
when (val index = decodeElementIndex(descriptor)) {
0 -> name = decodeStringElement(descriptor, index)
1 -> value = decodeSerializableElement(descriptor, index, JsonPrimitive.serializer())
2 -> nameLocalizations = decodeSerializableElement(descriptor, index, LocalizationSerializer)
2 -> nameLocalizations =
Optional(decodeSerializableElement(descriptor, index, localizationsSerializer))

CompositeDecoder.DECODE_DONE -> break
else -> throw SerializationException("unknown index: $index")
else -> throw SerializationException("Unexpected index: $index")
}
}

when {
value.isString -> StringChoice(name, nameLocalizations, value.content)
else -> value.longOrNull?.let { IntegerChoice(name, nameLocalizations, it) }
@OptIn(ExperimentalSerializationApi::class)
if (name == null || value == null) throw MissingFieldException(
missingFields = listOfNotNull("name".takeIf { name == null }, "value".takeIf { value == null }),
serialName = descriptor.serialName,
)

if (value.isString) {
StringChoice(name, nameLocalizations, value.content)
} else {
value.longOrNull?.let { IntegerChoice(name, nameLocalizations, it) }
?: value.doubleOrNull?.let { NumberChoice(name, nameLocalizations, it) }
?: throw SerializationException("Illegal choice value: $value")
}
}
}

override fun serialize(encoder: Encoder, value: Choice<*>) = encoder.encodeStructure(descriptor) {

encodeStringElement(descriptor, 0, value.name)

when (value) {
is IntegerChoice -> encodeLongElement(descriptor, 1, value.value)
is NumberChoice -> encodeDoubleElement(descriptor, 1, value.value)
is StringChoice -> encodeStringElement(descriptor, 1, value.value)
}

if (value.nameLocalizations !is Optional.Missing) {
encodeSerializableElement(descriptor, 2, LocalizationSerializer, value.nameLocalizations)
}
}
public companion object {
@Suppress("UNUSED_PARAMETER")
@Deprecated("Choice is no longer generic", ReplaceWith("this.serializer()"), DeprecationLevel.WARNING)
public fun <T0> serializer(typeSerial0: KSerializer<T0>): KSerializer<Choice> = serializer()
}
}

Expand Down Expand Up @@ -700,9 +710,19 @@ public data class DiscordGuildApplicationCommandPermission(
)

@Serializable
public data class DiscordAutoComplete<T>(
val choices: List<Choice<T>>
)
public data class DiscordAutoComplete(
val choices: List<Choice>,
) {
public companion object {
@Suppress("UNUSED_PARAMETER")
@Deprecated(
"DiscordAutoComplete is no longer generic",
ReplaceWith("this.serializer()"),
DeprecationLevel.WARNING,
)
public fun <T0> serializer(typeSerial0: KSerializer<T0>): KSerializer<DiscordAutoComplete> = serializer()
}
}

@Serializable
public data class DiscordModal(
Expand Down
1 change: 1 addition & 0 deletions core/api/core.api
Original file line number Diff line number Diff line change
Expand Up @@ -1784,6 +1784,7 @@ public final class dev/kord/core/behavior/interaction/AutoCompleteInteractionBeh
}

public final class dev/kord/core/behavior/interaction/AutoCompleteInteractionBehaviorKt {
public static final fun suggest (Ldev/kord/core/behavior/interaction/AutoCompleteInteractionBehavior;Ljava/util/List;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static final fun suggestInteger (Ldev/kord/core/behavior/interaction/AutoCompleteInteractionBehavior;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static final fun suggestNumber (Ldev/kord/core/behavior/interaction/AutoCompleteInteractionBehavior;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static final fun suggestString (Ldev/kord/core/behavior/interaction/AutoCompleteInteractionBehavior;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ public suspend inline fun AutoCompleteInteractionBehavior.suggestString(builder:
*
* The provided choices are only suggestions and the user can provide any other input as well.
*/
public suspend inline fun <reified T> AutoCompleteInteractionBehavior.suggest(choices: List<Choice<T>>) {
public suspend fun AutoCompleteInteractionBehavior.suggest(choices: List<Choice>) {
kord.rest.interaction.createAutoCompleteInteractionResponse(
id,
token,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ public data class ApplicationCommandOptionChoiceData(
val value: String
) {
public companion object {
public fun from(choice: Choice<*>): ApplicationCommandOptionChoiceData {
public fun from(choice: Choice): ApplicationCommandOptionChoiceData {
return with(choice) {
ApplicationCommandOptionChoiceData(name, value.toString())
}
Expand Down
7 changes: 5 additions & 2 deletions rest/api/rest.api
Original file line number Diff line number Diff line change
Expand Up @@ -2891,11 +2891,11 @@ public final class dev/kord/rest/json/request/AutoCompleteResponseCreateRequest
public final fun getType ()Ldev/kord/common/entity/InteractionResponseType;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
public static final synthetic fun write$Self (Ldev/kord/rest/json/request/AutoCompleteResponseCreateRequest;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;Lkotlinx/serialization/KSerializer;)V
public static final synthetic fun write$Self (Ldev/kord/rest/json/request/AutoCompleteResponseCreateRequest;Lkotlinx/serialization/encoding/CompositeEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;)V
}

public final class dev/kord/rest/json/request/AutoCompleteResponseCreateRequest$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public synthetic fun <init> (Lkotlinx/serialization/KSerializer;)V
public static final field INSTANCE Ldev/kord/rest/json/request/AutoCompleteResponseCreateRequest$$serializer;
public fun childSerializers ()[Lkotlinx/serialization/KSerializer;
public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ldev/kord/rest/json/request/AutoCompleteResponseCreateRequest;
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
Expand All @@ -2906,6 +2906,7 @@ public final class dev/kord/rest/json/request/AutoCompleteResponseCreateRequest$
}

public final class dev/kord/rest/json/request/AutoCompleteResponseCreateRequest$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
public final fun serializer (Lkotlinx/serialization/KSerializer;)Lkotlinx/serialization/KSerializer;
}

Expand Down Expand Up @@ -7389,6 +7390,8 @@ public final class dev/kord/rest/service/GuildServiceKt {

public final class dev/kord/rest/service/InteractionService : dev/kord/rest/service/RestService {
public fun <init> (Ldev/kord/rest/request/RequestHandler;)V
public final fun createAutoCompleteInteractionResponse (Ldev/kord/common/entity/Snowflake;Ljava/lang/String;Ldev/kord/common/entity/DiscordAutoComplete;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public final fun createBuilderAutoCompleteInteractionResponse (Ldev/kord/common/entity/Snowflake;Ljava/lang/String;Ldev/kord/rest/builder/interaction/BaseChoiceBuilder;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public final fun createFollowupMessage (Ldev/kord/common/entity/Snowflake;Ljava/lang/String;Ldev/kord/rest/json/request/MultipartFollowupMessageCreateRequest;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public final fun createFollowupMessage (Ldev/kord/common/entity/Snowflake;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static synthetic fun createFollowupMessage$default (Ldev/kord/rest/service/InteractionService;Ldev/kord/common/entity/Snowflake;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
Expand Down
10 changes: 5 additions & 5 deletions rest/src/commonMain/kotlin/builder/interaction/OptionsBuilder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,12 @@ public sealed class BaseChoiceBuilder<T>(
description: String,
type: ApplicationCommandOptionType
) : OptionsBuilder(name, description, type) {
// TODO We can change these types to Optional<MutableList<Choice<T>>> and MutableList<Choice<T>> once
// https://youtrack.jetbrains.com/issue/KT-51045 is fixed.
// The bug from that issue prevents you from setting BaseChoiceBuilder<*>.choices to `null`.
// TODO We can add another generic C : Choice and change these types to Optional<MutableList<C>> and MutableList<C>?
// once https://youtrack.jetbrains.com/issue/KT-51045 is fixed.
// The bug from that issue prevents you from setting BaseChoiceBuilder<*, *>.choices to `null`.
@Suppress("PropertyName")
internal var _choices: Optional<MutableList<Choice<*>>> = Optional.Missing()
public var choices: MutableList<Choice<*>>? by ::_choices.delegate()
internal var _choices: Optional<MutableList<Choice>> = Optional.Missing()
public var choices: MutableList<Choice>? by ::_choices.delegate()

public abstract fun choice(name: String, value: T, nameLocalizations: Optional<Map<Locale, String>?> = Optional.Missing())

Expand Down
18 changes: 15 additions & 3 deletions rest/src/commonMain/kotlin/json/request/InteractionsRequests.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import dev.kord.common.entity.*
import dev.kord.common.entity.optional.Optional
import dev.kord.common.entity.optional.OptionalBoolean
import dev.kord.rest.NamedFile
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

Expand Down Expand Up @@ -70,10 +71,21 @@ public data class InteractionResponseCreateRequest(
)

@Serializable
public data class AutoCompleteResponseCreateRequest<T>(
public data class AutoCompleteResponseCreateRequest(
val type: InteractionResponseType,
val data: DiscordAutoComplete<T>
)
val data: DiscordAutoComplete,
) {
public companion object {
@Suppress("UNUSED_PARAMETER")
@Deprecated(
"AutoCompleteResponseCreateRequest is no longer generic",
ReplaceWith("this.serializer()"),
DeprecationLevel.WARNING,
)
public fun <T0> serializer(typeSerial0: KSerializer<T0>): KSerializer<AutoCompleteResponseCreateRequest> =
serializer()
}
}

@Serializable
public data class ModalResponseCreateRequest(
Expand Down
25 changes: 17 additions & 8 deletions rest/src/commonMain/kotlin/service/InteractionService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -123,15 +123,27 @@ public class InteractionService(requestHandler: RequestHandler) : RestService(re
body(InteractionResponseCreateRequest.serializer(), request)
}

@Suppress("UNUSED_PARAMETER")
@Deprecated(
"DiscordAutoComplete is no longer generic and the typeSerializer argument is no longer needed.",
ReplaceWith("this.createAutoCompleteInteractionResponse(interactionId, interactionToken, autoComplete)"),
DeprecationLevel.WARNING,
)
public suspend inline fun <reified T> createAutoCompleteInteractionResponse(
interactionId: Snowflake,
interactionToken: String,
autoComplete: DiscordAutoComplete<T>,
autoComplete: DiscordAutoComplete,
typeSerializer: KSerializer<T> = serializer(),
): Unit = createAutoCompleteInteractionResponse(interactionId, interactionToken, autoComplete)

public suspend fun createAutoCompleteInteractionResponse(
interactionId: Snowflake,
interactionToken: String,
autoComplete: DiscordAutoComplete,
): Unit = call(Route.InteractionResponseCreate) {
interactionIdInteractionToken(interactionId, interactionToken)
body(
AutoCompleteResponseCreateRequest.serializer(typeSerializer),
AutoCompleteResponseCreateRequest.serializer(),
AutoCompleteResponseCreateRequest(
InteractionResponseType.ApplicationCommandAutoCompleteResult,
autoComplete
Expand Down Expand Up @@ -223,18 +235,15 @@ public class InteractionService(requestHandler: RequestHandler) : RestService(re
)
}

public suspend inline fun <reified T, Builder : BaseChoiceBuilder<T>> createBuilderAutoCompleteInteractionResponse(
public suspend inline fun <Builder : BaseChoiceBuilder<*>> createBuilderAutoCompleteInteractionResponse(
interactionId: Snowflake,
interactionToken: String,
builder: Builder,
builderFunction: Builder.() -> Unit
) {
// TODO We can remove this cast when we change the type of BaseChoiceBuilder.choices to MutableList<Choice<T>>.
// This can be done once https://youtrack.jetbrains.com/issue/KT-51045 is fixed.
// Until then this cast is necessary to get the right serializer through reified generics.
@Suppress("UNCHECKED_CAST")
val choices = (builder.apply(builderFunction).choices ?: emptyList()) as List<Choice<T>>
contract { callsInPlace(builderFunction, InvocationKind.EXACTLY_ONCE) }

val choices = builder.apply(builderFunction).choices ?: emptyList()
return createAutoCompleteInteractionResponse(interactionId, interactionToken, DiscordAutoComplete(choices))
}

Expand Down

0 comments on commit 2dad036

Please sign in to comment.