commonMain.entity.optional.Optional.kt Maven / Gradle / Ivy
package dev.kord.common.entity.optional
import dev.kord.common.entity.Snowflake
import dev.kord.common.entity.optional.Optional.*
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlin.js.JsName
import kotlin.jvm.JvmName
/**
* Represents a value that encapsulates all [three possible states of a value in the Discord API](https://discord.com/developers/docs/reference#nullable-and-optional-resource-fields).
* Specifically:
*
* * [Missing] - a field that was not present in the serialized entity
* * [Null] - a field that was assigned null in the serialized entity
* * [Value] - a field that was assigned a non-null value in the serialized entity.
*
* The base class is (de)serializable with kotlinx.serialization and should be used as follows:
*
* * `Optional` - a field that is only optional but not nullable.
* * `Optional` - A field that is both optional and nullable.
* * A field that is only nullable should be represented as `T?` instead.
*
* Trying to deserialize `null` as `Optional` will result in a [SerializationException] being thrown.
*
* Note that kotlinx.serialization does **not** call serializers for values that are not
* present in the serialized format. `Optional` fields should have a default value of `Optional.Missing`:
*
* ```kotlin
* @Serializable
* class DiscordUser(
* val id: Long,
* val username: String,
* val bot: Optional = Optional.Missing()
* )
* ```
*/
@Serializable(with = OptionalSerializer::class)
public sealed class Optional {
/**
* The value this optional wraps.
* * Both [Missing] and [Null] will always return `null`.
* * [Value] will always return a non-null value.
*/
public open val value: T?
get() = throw UnsupportedOperationException("This is implemented in implementation classes")
/**
* Represents a field that was not present in the serialized entity.
*/
public class Missing private constructor() : Optional() {
/**
* The value this optional wraps, always `null`.
*/
override val value: T?
get() = null
override fun toString(): String = "Optional.Missing"
override fun equals(other: Any?): Boolean {
return other is Missing<*>
}
override fun hashCode(): Int = 0
public companion object {
private val constantNull = Missing()
public operator fun invoke(): Missing = constantNull
}
}
/**
* Represents a field that was assigned null in the serialized entity.
*/
public class Null private constructor() : Optional() {
/**
* The value this optional wraps, always `null`.
*/
override val value: T?
get() = null
override fun toString(): String = "Optional.Null"
override fun equals(other: Any?): Boolean {
return other is Null<*>
}
override fun hashCode(): Int = 0
public companion object {
private val constantNull = Null()
public operator fun invoke(): Null = constantNull
}
}
/**
* Represents a field that was assigned a non-null value in the serialized entity.
* Equality and hashcode is implemented through its [value].
*
* @param value the value this optional wraps.
*/
public class Value(override val value: T) : Optional() {
override fun toString(): String = "Optional.Something(content=$value)"
/**
* Destructures this optional to its [value].
*/
public operator fun component1(): T = value
override fun equals(other: Any?): Boolean {
val value = other as? Value<*> ?: return false
return value.value == this.value
}
override fun hashCode(): Int = value.hashCode()
}
public companion object {
public fun > missingOnEmpty(value: C): Optional =
if (value.isEmpty()) Missing()
else Value(value)
/**
* Returns a [Missing] optional of type [T].
*/
public operator fun invoke(): Missing = Missing()
/**
* Returns a [Value] optional of type [T] with the given [value].
*/
public operator fun invoke(value: T): Value = Value(value)
/**
* Returns an [Optional] that is either [value] on a non-null [value], or [Null] on `null`.
*/
@JvmName("invokeNullable")
@JsName("invokeNullable")
public operator fun invoke(value: T?): Optional = when (value) {
null -> Null()
else -> Value(value)
}
}
internal class OptionalSerializer(private val contentSerializer: KSerializer) : KSerializer> {
override val descriptor: SerialDescriptor = contentSerializer.descriptor
@OptIn(ExperimentalSerializationApi::class)
override fun deserialize(decoder: Decoder): Optional {
/**
* let's clear up any inconsistencies, an Optional cannot be and be represented as nullable.
*/
if (!descriptor.isNullable && !decoder.decodeNotNullMark()) {
throw SerializationException("descriptor for ${descriptor.serialName} was not nullable but null mark was encountered")
}
/**
* This is rather ugly; I can't figure out a way to convince the compiler that isn't nullable,
* we have personally proven above that the serializer cannot return null so we'll just act as if we
* know what we're doing.
*/
val optional: Optional = when {
!decoder.decodeNotNullMark() -> {
decoder.decodeNull()
Null()
}
else -> Optional(decoder.decodeSerializableValue(contentSerializer))
}
@Suppress("UNCHECKED_CAST")
return optional as Optional
}
@OptIn(ExperimentalSerializationApi::class)
override fun serialize(encoder: Encoder, value: Optional) = when (value) {
is Missing<*> -> throw SerializationException("missing values cannot be serialized")
is Null<*> -> encoder.encodeNull()
is Value -> encoder.encodeSerializableValue(contentSerializer, value.value)
}
}
}
public fun Optional.switchOnMissing(value: T): Optional = when (this) {
is Missing -> Value(value)
is Null<*>, is Value -> this
}
public fun Optional.switchOnMissing(value: Optional): Optional = when (this) {
is Missing -> value
is Null<*>, is Value -> this
}
public fun Optional>.orEmpty(): List = when (this) {
is Missing, is Null<*> -> emptyList()
is Value -> value
}
public fun Optional>.orEmpty(): Set = when (this) {
is Missing, is Null<*> -> emptySet()
is Value -> value
}
@Suppress("UNCHECKED_CAST")
public inline fun Optional>.mapList(mapper: (E) -> T): Optional> = when (this) {
is Missing, is Null<*> -> this as Optional>
is Value -> Value(value.map(mapper))
}
@JvmName("mapNullableList")
public inline fun Optional?>.mapList(mapper: (E) -> T): Optional?> = when (this) {
is Missing -> Missing()
is Null -> Null()
is Value -> Value(value!!.map(mapper))
}
public fun Optional>.mapCopy(): Optional> = map { mutable -> mutable.toList() }
@JvmName("mapCopyOfMap")
public fun Optional>.mapCopy(): Optional
© 2015 - 2025 Weber Informatics LLC | Privacy Policy