diff --git a/README.md b/README.md index cf7adfb4..291ab4b9 100644 --- a/README.md +++ b/README.md @@ -80,8 +80,8 @@ fun Iterable.toPersistentSet(): PersistentSet #### `+` and `-` operators `plus` and `minus` operators on persistent collections exploit their immutability -and delegate the implementation to the collections themselves. -The operation is performed with persistence in mind: the returned immutable collection may share storage +and delegate the implementation to the collections themselves. +The operation is performed with persistence in mind: the returned immutable collection may share storage with the original collection. ```kotlin @@ -90,8 +90,8 @@ val newList = persistentListOf("a", "b") + "c" ``` > **Note:** you need to import these operators from `kotlinx.collections.immutable` package -in order for them to take the precedence over the ones from the -standard library. +> in order for them to take the precedence over the ones from the +> standard library. ```kotlin import kotlinx.collections.immutable.* @@ -112,6 +112,36 @@ With `mutate` it transforms to: collection.mutate { some_actions_on(it) } ``` +### Serialization + +Serialization modules allows you to apply custom immutable collection serializers, for example: + +```kotlin +@Serializable +private class MyCustomClass( + @Serializable(with = ImmutableMapSerializer::class) + val immutableMap: ImmutableMap +) +``` + +#### Collection Serializers + +| Serializer | Conversion method +|-------------------------------|------------------------- +| `ImmutableListSerializer` | `toImmutableList()` | +| `PersistentListSerializer` | `toPersistentList()` | +| `ImmutableSetSerializer` | `toImmutableSet()` | +| `PersistentSetSerializer` | `toPersistentSet()` | +| `PersistentHashSetSerializer` | `toPersistentHashSet()` | + +#### Map Serializers + +| Serializer | Conversion method +|-------------------------------|------------------------- +| `ImmutableMapSerializer` | `toImmutableMap()` | +| `PersistentMapSerializer` | `toPersistentMap()` | +| `PersistentHashMapSerializer` | `toPersistentHashMap()` | + ## Using in your projects > Note that the library is experimental and the API is subject to change. @@ -138,6 +168,7 @@ kotlin { commonMain { dependencies { implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.8") + implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable-serialization:0.3.8") } } } @@ -150,11 +181,19 @@ The Maven Central repository is available for dependency lookup by default. Add dependencies (you can also add other modules that you need): ```xml - - org.jetbrains.kotlinx - kotlinx-collections-immutable-jvm - 0.3.8 - + + + + org.jetbrains.kotlinx + kotlinx-collections-immutable-jvm + 0.3.8 + + + org.jetbrains.kotlinx + kotlinx-collections-immutable-serialization-jvm + 0.3.8 + + ``` ## Building from source diff --git a/serialization/build.gradle.kts b/serialization/build.gradle.kts new file mode 100644 index 00000000..9e62c349 --- /dev/null +++ b/serialization/build.gradle.kts @@ -0,0 +1,170 @@ +import kotlinx.team.infra.mavenPublicationsPom + +plugins { + id("kotlin-multiplatform") + `maven-publish` + kotlin("plugin.serialization") version "1.9.21" +} + +base { + archivesBaseName = "kotlinx-collections-immutable-serialization" // doesn't work +} + +mavenPublicationsPom { + description.set("Kotlin Immutable Collections serializers") +} + +kotlin { + applyDefaultHierarchyTemplate() + explicitApi() + + // According to https://kotlinlang.org/docs/native-target-support.html + // Tier 1 + linuxX64() + macosX64() + macosArm64() + iosSimulatorArm64() + iosX64() + + // Tier 2 + linuxArm64() + watchosSimulatorArm64() + watchosX64() + watchosArm32() + watchosArm64() + tvosSimulatorArm64() + tvosX64() + tvosArm64() + iosArm64() + + // Tier 3 + androidNativeArm32() + androidNativeArm64() + androidNativeX86() + androidNativeX64() + mingwX64() + watchosDeviceArm64() + + jvm { + compilations.all { + kotlinOptions { + jvmTarget = "1.8" + } + } + } + + js { + nodejs { + testTask { + useMocha { + timeout = "30000" + } + } + } + compilations.all { + kotlinOptions { + sourceMap = true + moduleKind = "umd" + metaInfo = true + } + } + } + + wasmJs { + nodejs { + testTask { + useMocha { + timeout = "30000" + } + } + } + } + + wasmWasi { + nodejs { + testTask { + useMocha { + timeout = "30000" + } + } + } + } + + sourceSets.all { + kotlin.setSrcDirs(listOf("$name/src")) + resources.setSrcDirs(listOf("$name/resources")) + languageSettings.apply { + // progressiveMode = true + optIn("kotlin.RequiresOptIn") + } + } + + sourceSets { + val commonMain by getting { + dependencies { + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3") + implementation(project(":kotlinx-collections-immutable")) + } + } + val commonTest by getting { + dependencies { + implementation(kotlin("test")) + } + } + + val jvmMain by getting { + } + val jvmTest by getting { + dependencies { + implementation("com.google.guava:guava-testlib:18.0") + } + } + + val jsMain by getting { + } + val jsTest by getting { + } + + val wasmMain by creating { + dependsOn(commonMain) + } + val wasmTest by creating { + dependsOn(commonTest) + } + + val wasmJsMain by getting { + dependsOn(wasmMain) + } + val wasmJsTest by getting { + dependsOn(wasmTest) + } + + val wasmWasiMain by getting { + dependsOn(wasmMain) + } + val wasmWasiTest by getting { + dependsOn(wasmTest) + } + + val nativeMain by getting { + } + val nativeTest by getting { + } + } +} + +tasks { + named("jvmTest", Test::class) { + maxHeapSize = "1024m" + } +} + +with(org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin.apply(rootProject)) { + nodeVersion = "21.0.0-v8-canary202309167e82ab1fa2" + nodeDownloadBaseUrl = "https://nodejs.org/download/v8-canary" +} + +// Drop this when node js version become stable +tasks.withType().configureEach { + args.add("--ignore-engines") +} \ No newline at end of file diff --git a/serialization/commonMain/src/kotlinx/collections/immutable/serialization/DefaultImmutableCollectionSerializer.kt b/serialization/commonMain/src/kotlinx/collections/immutable/serialization/DefaultImmutableCollectionSerializer.kt new file mode 100644 index 00000000..9d4c11a0 --- /dev/null +++ b/serialization/commonMain/src/kotlinx/collections/immutable/serialization/DefaultImmutableCollectionSerializer.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2016-2024 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.collections.immutable.serialization + +import kotlinx.collections.immutable.ImmutableCollection +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.ImmutableSet +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.PersistentSet +import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toImmutableSet +import kotlinx.collections.immutable.toPersistentHashSet +import kotlinx.collections.immutable.toPersistentList +import kotlinx.collections.immutable.toPersistentSet +import kotlinx.serialization.KSerializer +import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +internal class DefaultImmutableCollectionSerializer>( + serializer: KSerializer, + private val transform: (Collection) -> C +) : KSerializer { + private val listSerializer = ListSerializer(serializer) + + override val descriptor: SerialDescriptor = serializer.descriptor + + override fun serialize(encoder: Encoder, value: C) { + return listSerializer.serialize(encoder, value.toList()) + } + + override fun deserialize(decoder: Decoder): C { + return listSerializer.deserialize(decoder).let(transform) + } +} + +public class ImmutableListSerializer( + serializer: KSerializer +) : KSerializer> by DefaultImmutableCollectionSerializer( + serializer = serializer, + transform = { decodedList -> decodedList.toImmutableList() } +) + +public class PersistentListSerializer( + serializer: KSerializer +) : KSerializer> by DefaultImmutableCollectionSerializer( + serializer = serializer, + transform = { decodedList -> decodedList.toPersistentList() } +) + +public class ImmutableSetSerializer( + serializer: KSerializer +) : KSerializer> by DefaultImmutableCollectionSerializer( + serializer = serializer, + transform = { decodedList -> decodedList.toImmutableSet() } +) + +public class PersistentSetSerializer( + serializer: KSerializer +) : KSerializer> by DefaultImmutableCollectionSerializer( + serializer = serializer, + transform = { decodedList -> decodedList.toPersistentSet() } +) + +public class PersistentHashSetSerializer( + serializer: KSerializer +) : KSerializer> by DefaultImmutableCollectionSerializer( + serializer = serializer, + transform = { decodedList -> decodedList.toPersistentHashSet() } +) diff --git a/serialization/commonMain/src/kotlinx/collections/immutable/serialization/ImmutableMapSerializer.kt b/serialization/commonMain/src/kotlinx/collections/immutable/serialization/ImmutableMapSerializer.kt new file mode 100644 index 00000000..97fd9785 --- /dev/null +++ b/serialization/commonMain/src/kotlinx/collections/immutable/serialization/ImmutableMapSerializer.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2016-2024 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.collections.immutable.serialization + +import kotlinx.collections.immutable.ImmutableMap +import kotlinx.collections.immutable.PersistentMap +import kotlinx.collections.immutable.toImmutableMap +import kotlinx.collections.immutable.toPersistentHashMap +import kotlinx.collections.immutable.toPersistentMap +import kotlinx.serialization.KSerializer +import kotlinx.serialization.builtins.MapSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +internal class DefaultImmutableMapSerializer>( + keySerializer: KSerializer, + valueSerializer: KSerializer, + private val transform: (Map) -> M +) : KSerializer { + private val mapSerializer = MapSerializer(keySerializer, valueSerializer) + + override val descriptor: SerialDescriptor = mapSerializer.descriptor + + override fun serialize(encoder: Encoder, value: M) { + return mapSerializer.serialize(encoder, value.toMap()) + } + + override fun deserialize(decoder: Decoder): M { + return mapSerializer.deserialize(decoder).let(transform) + } +} + +public class ImmutableMapSerializer( + keySerializer: KSerializer, + valueSerializer: KSerializer, +) : KSerializer> by DefaultImmutableMapSerializer( + keySerializer = keySerializer, + valueSerializer = valueSerializer, + transform = { decodedMap -> decodedMap.toImmutableMap() } +) + +public class PersistentMapSerializer( + keySerializer: KSerializer, + valueSerializer: KSerializer, +) : KSerializer> by DefaultImmutableMapSerializer( + keySerializer = keySerializer, + valueSerializer = valueSerializer, + transform = { decodedMap -> decodedMap.toPersistentMap() } +) + +public class PersistentHashMapSerializer( + keySerializer: KSerializer, + valueSerializer: KSerializer, +) : KSerializer> by DefaultImmutableMapSerializer( + keySerializer = keySerializer, + valueSerializer = valueSerializer, + transform = { decodedMap -> decodedMap.toPersistentHashMap() } +) diff --git a/serialization/commonTest/src/kotlinx/collections/immutable/serialization/ImmutableCollectionSerializerTest.kt b/serialization/commonTest/src/kotlinx/collections/immutable/serialization/ImmutableCollectionSerializerTest.kt new file mode 100644 index 00000000..24421d57 --- /dev/null +++ b/serialization/commonTest/src/kotlinx/collections/immutable/serialization/ImmutableCollectionSerializerTest.kt @@ -0,0 +1,93 @@ +/* + * Copyright 2016-2024 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.collections.immutable.serialization + +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.ImmutableSet +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.PersistentSet +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.persistentSetOf +import kotlinx.collections.immutable.serialization.util.JsonConfigurationFactory +import kotlinx.collections.immutable.serialization.util.JsonExt.encodeAndDecode +import kotlinx.serialization.Serializable + +class ImmutableCollectionSerializerTest { + + @Serializable + private class ImmutableListHolder( + @Serializable(with = ImmutableListSerializer::class) + val immutableList: ImmutableList + ) + + @Test + fun testImmutableList() { + val json = JsonConfigurationFactory.createJsonConfiguration() + persistentListOf(1, 2, 3) + .let(::ImmutableListHolder) + .let { expectedList -> assertContentEquals(expectedList.immutableList, json.encodeAndDecode(expectedList).immutableList) } + } + + @Serializable + private class PersistentListHolder( + @Serializable(with = PersistentListSerializer::class) + val persistentList: PersistentList + ) + + @Test + fun testPersistentList() { + val json = JsonConfigurationFactory.createJsonConfiguration() + persistentListOf(1, 2, 3) + .let(::PersistentListHolder) + .let { expectedList -> assertContentEquals(expectedList.persistentList, json.encodeAndDecode(expectedList).persistentList) } + } + + @Serializable + private class ImmutableSetHolder( + @Serializable(with = ImmutableSetSerializer::class) + val immutableSet: ImmutableSet + ) + + @Test + fun testImmutableSet() { + val json = JsonConfigurationFactory.createJsonConfiguration() + persistentSetOf(1, 2, 3) + .let(::ImmutableSetHolder) + .let { expectedSet -> assertEquals(expectedSet.immutableSet, json.encodeAndDecode(expectedSet).immutableSet) } + } + + @Serializable + private class PersistentSetHolder( + @Serializable(with = PersistentSetSerializer::class) + val persistentSet: PersistentSet + ) + + @Test + fun testPersistentSet() { + val json = JsonConfigurationFactory.createJsonConfiguration() + persistentSetOf(1, 2, 3) + .let(::PersistentSetHolder) + .let { expectedSet -> assertEquals(expectedSet.persistentSet, json.encodeAndDecode(expectedSet).persistentSet) } + } + + @Serializable + private class PersistentHashSetHolder( + @Serializable(with = PersistentHashSetSerializer::class) + val persistentHashSet: PersistentSet + ) + + @Test + fun testPersistentHashSet() { + val json = JsonConfigurationFactory.createJsonConfiguration() + persistentSetOf(1, 2, 3) + .let(::PersistentHashSetHolder) + .let { expectedSet -> assertEquals(expectedSet.persistentHashSet, json.encodeAndDecode(expectedSet).persistentHashSet) } + } + +} \ No newline at end of file diff --git a/serialization/commonTest/src/kotlinx/collections/immutable/serialization/ImmutableMapSerializerTest.kt b/serialization/commonTest/src/kotlinx/collections/immutable/serialization/ImmutableMapSerializerTest.kt new file mode 100644 index 00000000..1d87e24a --- /dev/null +++ b/serialization/commonTest/src/kotlinx/collections/immutable/serialization/ImmutableMapSerializerTest.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2016-2024 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.collections.immutable.serialization + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlinx.collections.immutable.ImmutableMap +import kotlinx.collections.immutable.PersistentMap +import kotlinx.collections.immutable.persistentMapOf +import kotlinx.collections.immutable.serialization.util.JsonConfigurationFactory +import kotlinx.collections.immutable.serialization.util.JsonExt.encodeAndDecode +import kotlinx.serialization.Serializable + +class ImmutableMapSerializerTest { + + @Serializable + private class ImmutableMapHolder( + @Serializable(with = ImmutableMapSerializer::class) + val immutableMap: ImmutableMap + ) + + @Test + fun testImmutableList() { + val json = JsonConfigurationFactory.createJsonConfiguration() + persistentMapOf(1 to 1, 2 to 2, 3 to 3) + .let(::ImmutableMapHolder) + .let { expectedList -> assertEquals(expectedList.immutableMap, json.encodeAndDecode(expectedList).immutableMap) } + } + + @Serializable + private class PersistentMapHolder( + @Serializable(with = PersistentMapSerializer::class) + val persistentMap: PersistentMap + ) + + @Test + fun testPersistentMap() { + val json = JsonConfigurationFactory.createJsonConfiguration() + persistentMapOf(1 to 1, 2 to 2, 3 to 3) + .let(::PersistentMapHolder) + .let { expectedList -> assertEquals(expectedList.persistentMap, json.encodeAndDecode(expectedList).persistentMap) } + } + + @Serializable + private class PersistentHashMapHolder( + @Serializable(with = PersistentHashMapSerializer::class) + val hashMap: PersistentMap + ) + + @Test + fun testPersistentHashMap() { + val json = JsonConfigurationFactory.createJsonConfiguration() + persistentMapOf(1 to 1, 2 to 2, 3 to 3) + .let(::PersistentHashMapHolder) + .let { expectedList -> assertEquals(expectedList.hashMap, json.encodeAndDecode(expectedList).hashMap) } + } + + +} \ No newline at end of file diff --git a/serialization/commonTest/src/kotlinx/collections/immutable/serialization/util/JsonConfigurationFactory.kt b/serialization/commonTest/src/kotlinx/collections/immutable/serialization/util/JsonConfigurationFactory.kt new file mode 100644 index 00000000..59ec77d3 --- /dev/null +++ b/serialization/commonTest/src/kotlinx/collections/immutable/serialization/util/JsonConfigurationFactory.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2016-2024 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.collections.immutable.serialization.util + +import kotlinx.serialization.json.Json + +internal object JsonConfigurationFactory { + fun createJsonConfiguration() = Json { + isLenient = false + prettyPrint = true + ignoreUnknownKeys = false + } +} \ No newline at end of file diff --git a/serialization/commonTest/src/kotlinx/collections/immutable/serialization/util/JsonExt.kt b/serialization/commonTest/src/kotlinx/collections/immutable/serialization/util/JsonExt.kt new file mode 100644 index 00000000..baff1efe --- /dev/null +++ b/serialization/commonTest/src/kotlinx/collections/immutable/serialization/util/JsonExt.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2016-2024 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.collections.immutable.serialization.util + +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +object JsonExt { + inline fun Json.encodeAndDecode(value: T): T { + val string = encodeToString(value) + return decodeFromString(string) + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 1d4d5c9d..359ec154 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -10,6 +10,9 @@ rootProject.name = "Kotlin-Immutable-Collections" // TODO: Make readable name wh include(":core") project(":core").name="kotlinx-collections-immutable" +include(":serialization") +project(":serialization").name="kotlinx-collections-immutable-serialization" + include( ":benchmarks", ":benchmarks:runner"