diff --git a/arrow-libs/core/arrow-core-serialization/api/arrow-core-serialization.api b/arrow-libs/core/arrow-core-serialization/api/arrow-core-serialization.api index 2471e2079cd..3f398f5e73b 100644 --- a/arrow-libs/core/arrow-core-serialization/api/arrow-core-serialization.api +++ b/arrow-libs/core/arrow-core-serialization/api/arrow-core-serialization.api @@ -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; +} + diff --git a/arrow-libs/core/arrow-core-serialization/src/commonMain/kotlin/arrow/core/serialization/OptionSerializer.kt b/arrow-libs/core/arrow-core-serialization/src/commonMain/kotlin/arrow/core/serialization/OptionSerializer.kt index 68d1996f4ae..8bee2c44c2b 100644 --- a/arrow-libs/core/arrow-core-serialization/src/commonMain/kotlin/arrow/core/serialization/OptionSerializer.kt +++ b/arrow-libs/core/arrow-core-serialization/src/commonMain/kotlin/arrow/core/serialization/OptionSerializer.kt @@ -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( +public class OptionSerializer( elementSerializer: KSerializer ) : KSerializer> { private val nullableSerializer: KSerializer = elementSerializer.nullable @@ -20,3 +21,9 @@ public class OptionSerializer( override fun deserialize(decoder: Decoder): Option = nullableSerializer.deserialize(decoder).toOption() } + +@OptIn(ExperimentalSerializationApi::class) +private val KSerializer.nullable get() = + @Suppress("UNCHECKED_CAST") + if (descriptor.isNullable) (this as KSerializer) + else (this as KSerializer).nullable diff --git a/arrow-libs/core/arrow-core-serialization/src/commonMain/kotlin/arrow/core/serialization/SerializersModule.kt b/arrow-libs/core/arrow-core-serialization/src/commonMain/kotlin/arrow/core/serialization/SerializersModule.kt new file mode 100644 index 00000000000..1f2e8425ba4 --- /dev/null +++ b/arrow-libs/core/arrow-core-serialization/src/commonMain/kotlin/arrow/core/serialization/SerializersModule.kt @@ -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) } +} + diff --git a/arrow-libs/core/arrow-core-serialization/src/commonTest/kotlin/arrow/core/serialization/BackAgainTest.kt b/arrow-libs/core/arrow-core-serialization/src/commonTest/kotlin/arrow/core/serialization/BackAgainTest.kt index f76c7c0c804..11976fcea67 100644 --- a/arrow-libs/core/arrow-core-serialization/src/commonTest/kotlin/arrow/core/serialization/BackAgainTest.kt +++ b/arrow-libs/core/arrow-core-serialization/src/commonTest/kotlin/arrow/core/serialization/BackAgainTest.kt @@ -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 @@ -47,11 +49,11 @@ data class NonEmptyListInside(val thing: NonEmptyList) @Serializable data class NonEmptySetInside(val thing: NonEmptySet) -inline fun backAgain(generator: Arb) = +inline fun backAgain(generator: Arb, json: Json = Json) = runTest { checkAll(generator) { e -> - val result = Json.encodeToJsonElement(e) - val back = Json.decodeFromJsonElement(result) + val result = json.encodeToJsonElement(e) + val back = json.decodeFromJsonElement(result) back shouldBe e } } @@ -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 = OptionInside(Some(null)) + val result = Json.encodeToJsonElement>(container) + val back = Json.decodeFromJsonElement>(result) + back shouldBe OptionInside(None) // not `container` + } } diff --git a/arrow-libs/core/arrow-core-serialization/src/commonTest/kotlin/arrow/core/serialization/ModuleTest.kt b/arrow-libs/core/arrow-core-serialization/src/commonTest/kotlin/arrow/core/serialization/ModuleTest.kt new file mode 100644 index 00000000000..b4c4a3360ce --- /dev/null +++ b/arrow-libs/core/arrow-core-serialization/src/commonTest/kotlin/arrow/core/serialization/ModuleTest.kt @@ -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(@Contextual val thing: Either) + +@Serializable +data class ContextualIorInside(@Contextual val thing: Ior) + +@Serializable +data class ContextualOptionInside(@Contextual val thing: Option) + +@Serializable +data class ContextualNonEmptyListInside(@Contextual val thing: NonEmptyList) + +@Serializable +data class ContextualNonEmptySetInside(@Contextual val thing: NonEmptySet) + +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) +}