Skip to content

Instantly share code, notes, and snippets.

@ephemient
Last active March 24, 2022 21:58
Show Gist options
  • Save ephemient/65a3ee700f020661858c89b5648a2772 to your computer and use it in GitHub Desktop.
Save ephemient/65a3ee700f020661858c89b5648a2772 to your computer and use it in GitHub Desktop.
import android.os.Parcel
import kotlinx.parcelize.Parceler
import kotlinx.serialization.BinaryFormat
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
/**
* This class allows any [kotlinx.serialization.Serializable] type to be easily used within a
* [kotlinx.parcelize.Parcelize] type without implementing [android.os.Parcelable].
*
* For example, given
* ```kotlin
* @Serializable
* data class Foo(...) // not Parcelable
* ```
* a [Parceler] is all that is needed to make it usable in
* ```kotlin
* @Parcelize
* @TypeParceler<Foo, FooParceler>
* data class Bar(val foo: @WriteWith<FooParceler> Foo)
*
* internal object FooParceler : SerializationParceler<Foo> {
* override val serializer = Foo.serializer()
* }
* ```
* where only one of {[kotlinx.parcelize.TypeParceler], [kotlinx.parcelize.WriteWith]} is needed.
*/
abstract class SerializationParceler<T> : Parceler<T> {
abstract val serializer: KSerializer<T>
open val binaryFormat: BinaryFormat
get() = simpleBinaryFormat
override fun T.write(parcel: Parcel, flags: Int) {
val bytes = binaryFormat.encodeToByteArray(serializer, this)
parcel.writeInt(bytes.size)
parcel.writeByteArray(bytes)
}
override fun create(parcel: Parcel): T {
val bytes = ByteArray(parcel.readInt())
parcel.readByteArray(bytes)
return binaryFormat.decodeFromByteArray(serializer, bytes)
}
companion object {
@OptIn(ExperimentalSerializationApi::class)
private val simpleBinaryFormat: BinaryFormat = SimpleBinaryFormat()
}
}
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.DataInput
import java.io.DataInputStream
import java.io.DataOutput
import java.io.DataOutputStream
import kotlinx.serialization.BinaryFormat
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerializationException
import kotlinx.serialization.SerializationStrategy
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.AbstractDecoder
import kotlinx.serialization.encoding.AbstractEncoder
import kotlinx.serialization.encoding.CompositeDecoder.Companion.DECODE_DONE
import kotlinx.serialization.modules.EmptySerializersModule
import kotlinx.serialization.modules.SerializersModule
@ExperimentalSerializationApi
class SimpleBinaryFormat constructor(
override val serializersModule: SerializersModule = EmptySerializersModule,
) : BinaryFormat {
override fun <T> encodeToByteArray(serializer: SerializationStrategy<T>, value: T): ByteArray =
ByteArrayOutputStream().apply {
DataOutputStream(this).use { encodeTo(serializer, value, it) }
}.toByteArray()
fun <T> encodeTo(serializer: SerializationStrategy<T>, value: T, output: DataOutput) {
BinaryEncoder(output).encodeSerializableValue(serializer, value)
}
override fun <T> decodeFromByteArray(deserializer: DeserializationStrategy<T>, bytes: ByteArray): T =
with(ByteArrayInputStream(bytes)) {
DataInputStream(this).use { decodeFrom(deserializer, it) }.also { check(available() == 0) }
}
fun <T> decodeFrom(deserializer: DeserializationStrategy<T>, input: DataInput): T =
BinaryDecoder(input).decodeSerializableValue(deserializer)
private inner class BinaryEncoder(private val output: DataOutput) : AbstractEncoder() {
override val serializersModule: SerializersModule
get() = this@SimpleBinaryFormat.serializersModule
override fun endStructure(descriptor: SerialDescriptor) = output.writeInt(DECODE_DONE)
override fun encodeElement(descriptor: SerialDescriptor, index: Int): Boolean =
output.writeInt(index).let { true }
override fun encodeNull() = encodeBoolean(false)
override fun encodeNotNullMark() = encodeBoolean(true)
override fun encodeBoolean(value: Boolean) = output.writeBoolean(value)
override fun encodeByte(value: Byte) = output.writeByte(value.toInt())
override fun encodeShort(value: Short) = output.writeShort(value.toInt())
override fun encodeInt(value: Int) = output.writeInt(value)
override fun encodeLong(value: Long) = output.writeLong(value)
override fun encodeFloat(value: Float) = output.writeFloat(value)
override fun encodeDouble(value: Double) = output.writeDouble(value)
override fun encodeChar(value: Char) = output.writeChar(value.code)
override fun encodeString(value: String) = output.writeUTF(value)
override fun encodeEnum(enumDescriptor: SerialDescriptor, index: Int) = encodeInt(index)
}
private inner class BinaryDecoder(private val input: DataInput) : AbstractDecoder() {
private var isEnded = false
override val serializersModule: SerializersModule
get() = this@SimpleBinaryFormat.serializersModule
override fun endStructure(descriptor: SerialDescriptor) {
val index = if (isEnded) DECODE_DONE else input.readInt()
if (index != DECODE_DONE) throw SerializationException("Unexpected index: $index")
isEnded = false
}
override fun decodeElementIndex(descriptor: SerialDescriptor): Int =
input.readInt().also { isEnded = it == DECODE_DONE }
override fun decodeNotNullMark(): Boolean = decodeBoolean()
override fun decodeBoolean(): Boolean = input.readBoolean()
override fun decodeByte(): Byte = input.readByte()
override fun decodeShort(): Short = input.readShort()
override fun decodeInt(): Int = input.readInt()
override fun decodeLong(): Long = input.readLong()
override fun decodeFloat(): Float = input.readFloat()
override fun decodeDouble(): Double = input.readDouble()
override fun decodeChar(): Char = input.readChar()
override fun decodeString(): String = input.readUTF()
override fun decodeEnum(enumDescriptor: SerialDescriptor): Int = decodeInt()
}
}
@file:OptIn(ExperimentalSerializationApi::class)
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Polymorphic
import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.builtins.MapSerializer
import kotlinx.serialization.builtins.nullable
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.polymorphic
import kotlinx.serialization.modules.subclass
import org.junit.Assert.assertEquals
import org.junit.Test
class SimpleBinaryFormatTest {
private val format = SimpleBinaryFormat(
SerializersModule {
polymorphic(Interface::class) {
subclass(Simple.serializer())
subclass(Singleton.serializer())
}
}
)
@Test
fun builtin() {
testRoundTrip(Boolean.serializer(), true)
testRoundTrip(Byte.serializer(), -59)
testRoundTrip(Short.serializer(), 2600)
testRoundTrip(Int.serializer(), 2137353318)
testRoundTrip(Long.serializer(), -4982089500409860083)
testRoundTrip(Float.serializer(), kotlin.math.E.toFloat())
testRoundTrip(Double.serializer(), kotlin.math.PI)
testRoundTrip(Char.serializer(), '※')
testRoundTrip(String.serializer(), "Hello, world!")
}
@Test
fun simple() {
testRoundTrip(Simple.serializer(), Simple(42, "forty-two"))
testRoundTrip(Simple.serializer(), Simple(60))
}
@Test
fun generic() {
testRoundTrip(Holder.serializer(Simple.serializer()), Holder(Simple(42, "forty-two")))
}
@Test
fun nullable() {
testRoundTrip(Simple.serializer().nullable, Simple(0))
testRoundTrip(Simple.serializer().nullable, null)
testRoundTrip(Nullable.serializer(), Nullable(Simple(0)))
testRoundTrip(Nullable.serializer(), Nullable(null))
}
@Test
fun collection() {
val list = listOf(Simple(1), Simple(2), Simple(3))
val map = mapOf("a" to Simple(4), "b" to Simple(5), "c" to Simple(6))
testRoundTrip(ListSerializer(Simple.serializer()), list)
testRoundTrip(MapSerializer(String.serializer(), Simple.serializer()), map)
testRoundTrip(Container.serializer(), Container(list, map))
}
@Test
fun singleton() {
testRoundTrip(Singleton.serializer(), Singleton)
}
@Test
fun polymorphic() {
testRoundTrip(Wrapper.serializer(), Wrapper(Simple(42, "forty-two"), Sealed.Data(100)))
testRoundTrip(Wrapper.serializer(), Wrapper(Singleton, Sealed.Object))
}
private fun <T> testRoundTrip(serializer: KSerializer<T>, data: T) {
val bytes = format.encodeToByteArray(serializer, data)
println("$data -> " + bytes.joinToString(" ") { "%02X".format(it) })
assertEquals(data, format.decodeFromByteArray(serializer, bytes))
}
}
private interface Interface
@Serializable
private data class Simple(val first: Int, val second: String = "second") : Interface
@Serializable
private data class Holder<T>(val value: T)
@Serializable
private data class Nullable(val nullable: Simple?) : Interface
@Serializable
private data class Container(val list: List<Simple>, val map: Map<String, Simple>) : Interface
@Serializable
private object Singleton : Interface
@Serializable
sealed class Sealed {
@Serializable
data class Data(val id: Int) : Sealed()
@Serializable
object Object : Sealed()
}
@Serializable
private data class Wrapper(@Polymorphic val open: Interface, val closed: Sealed)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment