Skip to content

ATProto string ref type with known values: example Kotlin implementation #2

@orual

Description

@orual

Here's my take on the implementation of this pattern from the lexicons which makes for nice and usable Kotlin types while still serializing/deserializing correctly.

Essentially, these two classes exist already, hand-written, in Butterfly somewhere (currently in Types.kt).

@Serializable
abstract class Union<T> {
    abstract val value: T
}

open class StringUnionSerializer<U: Union<String>>(
    private val from: (String) -> U,
): KSerializer<U> {
    override val descriptor: SerialDescriptor = String.serializer().descriptor
    override fun deserialize(decoder: Decoder): U {
        return from(decoder.decodeString())
    }

    override fun serialize(encoder: Encoder, value: U) {
        encoder.encodeString(value.value)
    }
}

And then for lexicon JSON like this, snipped from com.atproto.label.defs.json:

"labelValue": {
  "type": "string",
  "knownValues": [
    "!hide",
    "!no-promote",
    "!warn",
    "!no-unauthenticated",
    "dmca-violation",
    "doxxing",
    "porn",
    "sexual",
    "nudity",
    "nsfl",
    "gore"
  ]
}

We generate the following Kotlin code.

object LabelValueSerializer: StringUnionSerializer<LabelValue>({ LabelValue.from(it) })

@Serializable(with = LabelValueSerializer::class)
sealed class LabelValue(override val value: String): Union<String>() {
    companion object {
        inline fun <reified U: LabelValue> from(value: String): U = when(value) {
            Warn.value -> Warn as U
            NoPromote.value -> NoPromote as U
            NoSelf.value -> NoSelf as U
            NoUnauthenticated.value -> NoUnauthenticated as U
            Hide.value -> Hide as U
            DMCAViolation.value -> DMCAViolation as U
            Doxxing.value -> Doxxing as U
            Porn.value -> Porn as U
            Sexual.value -> Sexual as U
            Nudity.value -> Nudity as U
            NSFL.value -> NSFL as U
            Gore.value -> Gore as U
            GraphicMedia.value -> GraphicMedia as U
            else -> Custom(value) as U
        }
    }

    data object Warn: LabelValue("!warn")
    data object NoPromote: LabelValue("!no-promote")
    data object NoUnauthenticated: LabelValue("!no-unauthenticated")
    data object NoSelf: LabelValue("!no-self")
    data object Hide: LabelValue("!hide")
    data object DMCAViolation: LabelValue("dmca-violation")
    data object Doxxing: LabelValue("doxxing")
    data object Porn: LabelValue("porn")
    data object Sexual: LabelValue("sexual")
    data object Nudity: LabelValue("nudity")
    data object NSFL: LabelValue("nsfl")
    data object Gore: LabelValue("gore")
    data object GraphicMedia: LabelValue("graphic-media")
    data class Custom(override val value: String): LabelValue(value)

    override fun toString(): String = value
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is LabelValue) return false
        if (value != other.value) return false
        return true
    }
    override fun hashCode(): Int {
        return value.hashCode()
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions