Skip to content

Commit

Permalink
add arrow-core-serialization SerializersModule (#3413)
Browse files Browse the repository at this point in the history
* feat: add SerializersModule for contextual/root usage of Arrow types

not including Option due to type restriction

* feat: remove `Any` restriction on `OptionSerializer`

allows adding `Option` support to `ArrowModule`
  • Loading branch information
tKe authored May 12, 2024
1 parent 2860c12 commit 8d0ce46
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,7 @@ public final class arrow/core/serialization/OptionSerializer : kotlinx/serializa
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
}

public final class arrow/core/serialization/SerializersModuleKt {
public static final fun getArrowModule ()Lkotlinx/serialization/modules/SerializersModule;
}

Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ package arrow.core.serialization

import arrow.core.Option
import arrow.core.toOption
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.nullable
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder

public class OptionSerializer<T : Any>(
public class OptionSerializer<T>(
elementSerializer: KSerializer<T>
) : KSerializer<Option<T>> {
private val nullableSerializer: KSerializer<T?> = elementSerializer.nullable
Expand All @@ -20,3 +21,9 @@ public class OptionSerializer<T : Any>(
override fun deserialize(decoder: Decoder): Option<T> =
nullableSerializer.deserialize(decoder).toOption()
}

@OptIn(ExperimentalSerializationApi::class)
private val <T> KSerializer<T>.nullable get() =
@Suppress("UNCHECKED_CAST")
if (descriptor.isNullable) (this as KSerializer<T?>)
else (this as KSerializer<T & Any>).nullable
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package arrow.core.serialization

import arrow.core.*
import kotlinx.serialization.modules.SerializersModule

public val ArrowModule: SerializersModule = SerializersModule {
contextual(Either::class) { (a, b) -> EitherSerializer(a, b) }
contextual(Ior::class) { (a, b) -> IorSerializer(a, b) }
contextual(NonEmptyList::class) { (t) -> NonEmptyListSerializer(t) }
contextual(NonEmptySet::class) { (t) -> NonEmptySetSerializer(t) }
contextual(Option::class) { (t) -> OptionSerializer(t) }
}

Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ import arrow.core.Either
import arrow.core.Ior
import arrow.core.NonEmptyList
import arrow.core.NonEmptySet
import arrow.core.None
import arrow.core.Option
import arrow.core.Some
import io.kotest.matchers.shouldBe
import io.kotest.property.Arb
import io.kotest.property.checkAll
Expand Down Expand Up @@ -47,11 +49,11 @@ data class NonEmptyListInside<A>(val thing: NonEmptyList<A>)
@Serializable
data class NonEmptySetInside<A>(val thing: NonEmptySet<A>)

inline fun <reified T> backAgain(generator: Arb<T>) =
inline fun <reified T> backAgain(generator: Arb<T>, json: Json = Json) =
runTest {
checkAll(generator) { e ->
val result = Json.encodeToJsonElement<T>(e)
val back = Json.decodeFromJsonElement<T>(result)
val result = json.encodeToJsonElement<T>(e)
val back = json.decodeFromJsonElement<T>(result)
back shouldBe e
}
}
Expand All @@ -71,4 +73,12 @@ class BackAgainTest {
backAgain(Arb.nonEmptyList(Arb.int()).map(::NonEmptyListInside))
@Test fun backAgainNonEmptySet() =
backAgain(Arb.nonEmptySet(Arb.int()).map(::NonEmptySetInside))

// capturing the current functionality of the OptionSerializer
@Test fun backAgainFlattensSomeNullToNone() {
val container: OptionInside<String?> = OptionInside(Some(null))
val result = Json.encodeToJsonElement<OptionInside<String?>>(container)
val back = Json.decodeFromJsonElement<OptionInside<String?>>(result)
back shouldBe OptionInside(None) // not `container`
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package arrow.core.serialization

import arrow.core.*
import io.kotest.matchers.shouldBe
import io.kotest.property.Arb
import io.kotest.property.arbitrary.int
import io.kotest.property.arbitrary.map
import io.kotest.property.arbitrary.string
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.nullable
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromJsonElement
import kotlinx.serialization.json.encodeToJsonElement
import kotlinx.serialization.modules.SerializersModule
import kotlin.test.Test


@Serializable
data class ContextualEitherInside<A, B>(@Contextual val thing: Either<A, B>)

@Serializable
data class ContextualIorInside<A, B>(@Contextual val thing: Ior<A, B>)

@Serializable
data class ContextualOptionInside<A>(@Contextual val thing: Option<A>)

@Serializable
data class ContextualNonEmptyListInside<A>(@Contextual val thing: NonEmptyList<A>)

@Serializable
data class ContextualNonEmptySetInside<A>(@Contextual val thing: NonEmptySet<A>)

class ModuleTest {
private val jsonWithModule = Json {
serializersModule = SerializersModule {
include(ArrowModule)
}
}

@Test
fun backAgainEither() =
backAgain(Arb.either(Arb.string(), Arb.int()), jsonWithModule)

@Test
fun backAgainIor() =
backAgain(Arb.ior(Arb.string(), Arb.int()), jsonWithModule)

@Test
fun backAgainOption() =
backAgain(Arb.option(Arb.string()), jsonWithModule)

@Test
fun backAgainNonEmptyList() =
backAgain(Arb.nonEmptyList(Arb.int()), jsonWithModule)

@Test
fun backAgainNonEmptySet() =
backAgain(Arb.nonEmptySet(Arb.int()), jsonWithModule)

@Test
fun backAgainContextualEither() =
backAgain(Arb.either(Arb.string(), Arb.int()).map(::ContextualEitherInside), jsonWithModule)

@Test
fun backAgainContextualIor() =
backAgain(Arb.ior(Arb.string(), Arb.int()).map(::ContextualIorInside), jsonWithModule)

@Test
fun backAgainContextualOption() =
backAgain(Arb.option(Arb.string()).map(::ContextualOptionInside), jsonWithModule)

@Test
fun backAgainContextualNonEmptyList() =
backAgain(Arb.nonEmptyList(Arb.int()).map(::ContextualNonEmptyListInside), jsonWithModule)

@Test
fun backAgainContextualNonEmptySet() =
backAgain(Arb.nonEmptySet(Arb.int()).map(::ContextualNonEmptySetInside), jsonWithModule)
}

0 comments on commit 8d0ce46

Please sign in to comment.