From e1f01c4e2d0c99bf015c6ec134f623da193ccc98 Mon Sep 17 00:00:00 2001 From: Adrian Kuta Date: Sat, 6 Jun 2026 13:47:20 +0200 Subject: [PATCH] feat: Kotlin 2.x/K2, version catalog, serialization & coroutines modules, docs v3.4 modernization (continued): - Migrate to Kotlin 2.x (K2); introduce gradle/libs.versions.toml version catalog; simplify the JS/Wasm/Node and iOS source-set wiring for the K2 hierarchy template. - Apply binary-compatibility-validator and Kover plugins. - New published module tree-structure-serialization: @Serializable TreeNodeDto with toDto()/toTreeNode() round-trip (kotlinx.serialization). - New published module tree-structure-coroutines: asFlow()/pre/post/levelOrderFlow() (kotlinx.coroutines Flow traversal). - Docs: README examples for Sequence/navigation/functional APIs, class-level KDoc (thread-safety/complexity), and a CHANGELOG.md. - Ignore subproject build/ directories. - Bump version to 3.4.0. All JVM tests green (core + both modules). --- .gitignore | 1 + CHANGELOG.md | 51 +++++++++++ README.md | 55 +++++++++++- build.gradle.kts | 81 ++++------------- gradle.properties | 1 - gradle/libs.versions.toml | 21 +++++ settings.gradle.kts | 3 +- .../datastructure/tree/TreeNode.kt | 18 +++- tree-structure-coroutines/build.gradle.kts | 88 +++++++++++++++++++ .../tree/coroutines/TreeNodeFlowExt.kt | 24 +++++ .../tree/coroutines/TreeNodeFlowTest.kt | 35 ++++++++ tree-structure-serialization/build.gradle.kts | 88 +++++++++++++++++++ .../tree/serialization/TreeNodeDto.kt | 30 +++++++ .../TreeNodeSerializationTest.kt | 40 +++++++++ 14 files changed, 470 insertions(+), 66 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 gradle/libs.versions.toml create mode 100644 tree-structure-coroutines/build.gradle.kts create mode 100644 tree-structure-coroutines/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/coroutines/TreeNodeFlowExt.kt create mode 100644 tree-structure-coroutines/src/commonTest/kotlin/com.github.adriankuta/datastructure/tree/coroutines/TreeNodeFlowTest.kt create mode 100644 tree-structure-serialization/build.gradle.kts create mode 100644 tree-structure-serialization/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/serialization/TreeNodeDto.kt create mode 100644 tree-structure-serialization/src/commonTest/kotlin/com.github.adriankuta/datastructure/tree/serialization/TreeNodeSerializationTest.kt diff --git a/.gitignore b/.gitignore index 99efa87..f1f7578 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ /.idea/assetWizardSettings.xml .DS_Store /build +build/ /captures .externalNativeBuild .cxx diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3c031be --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,51 @@ +# Changelog + +All notable changes to this project are documented here. The format is based on +[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to +[Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [3.4.0] + +### Added +- Lazy `Sequence` traversal: `asSequence(order)`, `preOrderSequence()`, `postOrderSequence()`, + `levelOrderSequence()` — composes with the Kotlin stdlib and short-circuits. +- Navigation extensions: `isLeaf`, `degree`, `root()`, `ancestors()`, `siblings()`, `leaves()`, + `descendants()`. +- Functional extensions: `findNode`, `filterNodes`, `anyNode`, `allNodes`, `countNodes`, + `foldNodes`, `mapValues`, `deepCopy`, `structurallyEquals` (all stack-safe). +- New optional modules published as separate artifacts: + - `tree-structure-serialization` — `kotlinx.serialization` support via a `TreeNodeDto`. + - `tree-structure-coroutines` — `Flow` traversal (`asFlow`, `preOrderFlow`, …). +- `CHANGELOG.md`, expanded README examples, and class-level KDoc (thread-safety / complexity). + +### Changed +- `nodeCount()`, `height()`, `clear()` and the post-order iterator are now iterative — deep or + degenerate (linear) trees no longer throw `StackOverflowError`. +- Migrated to Kotlin 2.x (K2 compiler) and introduced a Gradle version catalog. +- Build now uses `binary-compatibility-validator` (committed `.api` baselines) and Kover. + +## [3.1.5] + +### Fixed +- Removed a stray `println` in `TreeNode.removeChild()` that printed to stdout on every removal. + +### Removed +- Deleted the `Example.ws.kts` worksheet and the `kotlin("script-runtime")` dependency from the + published JVM artifact, plus the leftover `ExampleUnitTest` template test. + +### Changed +- Bumped `actions/checkout` v2 → v4 in CI workflows. + +## [3.1.4] +- Updated Kotlin and JS dependencies; added the `wasmJs` target. + +## [3.1.3] +- iOS targets and Maven Central (Sonatype Central Portal) publishing. + +[Unreleased]: https://github.com/AdrianKuta/Tree-Data-Structure/compare/v3.4.0...HEAD +[3.4.0]: https://github.com/AdrianKuta/Tree-Data-Structure/compare/v3.1.5...v3.4.0 +[3.1.5]: https://github.com/AdrianKuta/Tree-Data-Structure/compare/v3.1.3...v3.1.5 +[3.1.4]: https://github.com/AdrianKuta/Tree-Data-Structure/releases/tag/v3.1.4 +[3.1.3]: https://github.com/AdrianKuta/Tree-Data-Structure/releases/tag/v3.1.3 diff --git a/README.md b/README.md index 49c7bff..13a3036 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,12 @@ Lightweight Kotlin Multiplatform tree data structure for Kotlin and Java. Includes a small DSL, multiple traversal iterators, and pretty-print support. -- Kotlin Multiplatform (JVM, JS, iOS, and Native host) +- Kotlin Multiplatform (JVM, JS, Wasm, iOS, and Native host) - Pre-order, Post-order, and Level-order iteration +- Lazy `Sequence` traversal that composes with the Kotlin stdlib (`map`/`filter`/`firstOrNull`…) +- Navigation helpers: `root()`, `ancestors()`, `siblings()`, `leaves()`, `descendants()`, `isLeaf`, `degree` +- Functional helpers: `findNode`, `filterNodes`, `anyNode`, `allNodes`, `foldNodes`, `mapValues`, `deepCopy`, `structurallyEquals` +- Stack-safe: traversal and `height()`/`nodeCount()`/`clear()` handle arbitrarily deep trees without `StackOverflowError` - Simple DSL: tree { child(...) } - Utilities: nodeCount(), height(), depth(), path(), prettyString(), clear(), removeChild() @@ -116,6 +120,55 @@ root.removeChild(child) root.clear() // remove entire subtree ``` +### Lazy traversal with Sequence + +Traversal is exposed as a lazy `Sequence`, so it composes with the Kotlin standard library and +short-circuits (it never materializes the whole tree just to find one node): + +```kotlin +val tree = tree("World") { + child("North America") { child("USA") } + child("Europe") { + child("Poland") + child("Germany") + } +} + +// Pick an order explicitly — no need to mutate the node's state. +tree.preOrderSequence().map { it.value }.toList() // [World, North America, USA, Europe, Poland, Germany] +tree.levelOrderSequence().first { it.value == "USA" } // stops as soon as it's found +tree.asSequence(TreeNodeIterators.PostOrder).count() // 6 +``` + +### Navigation + +```kotlin +val usa = tree.findNode { it == "USA" }!! + +usa.isLeaf // true +usa.depth() // 2 +usa.root().value // "World" +usa.ancestors().map { it.value } // [North America, World] +tree.leaves().map { it.value } // [USA, Poland, Germany] +val europe = tree.findNode { it == "Europe" }!! +europe.children.first().siblings().map { it.value } // [Germany] +``` + +### Functional operations + +```kotlin +tree.anyNode { it == "Poland" } // true +tree.filterNodes { it.length > 5 } // nodes whose value is longer than 5 chars +tree.countNodes { it.startsWith("U") } // 1 + +// Transform values into a brand-new tree (the original is untouched); stack-safe. +val lengths: TreeNode = tree.mapValues { it.length } + +// Deep copy + structural comparison. +val copy = tree.deepCopy() +tree.structurallyEquals(copy) // true (same values, same shape, different nodes) +``` + ## Publishing to Maven Central (central.sonatype.com) This project is configured to publish artifacts to Maven Central via the Sonatype Central Portal. diff --git a/build.gradle.kts b/build.gradle.kts index 4eca99e..b64249c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,19 +1,17 @@ -import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl -import org.jetbrains.kotlin.gradle.targets.js.dsl.KotlinWasmTargetDsl -import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrTarget -import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension -import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl plugins { - kotlin("multiplatform") version "1.9.24" - id("org.jetbrains.dokka") version "1.9.20" - id("com.vanniktech.maven.publish") version "0.34.0" + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.dokka) + alias(libs.plugins.mavenPublish) + alias(libs.plugins.binaryCompatibilityValidator) + alias(libs.plugins.kover) signing } val PUBLISH_GROUP_ID = "com.github.adriankuta" val PUBLISH_ARTIFACT_ID = "tree-structure" // base artifact; KMP will add -jvm, -ios*, etc. -val PUBLISH_VERSION = "3.1.5" +val PUBLISH_VERSION = "3.4.0" val snapshot: String? by project @@ -29,7 +27,10 @@ mavenPublishing { pom { name.set("Tree Data Structure") - description.set("Simple implementation to store object in tree structure.") + description.set( + "Lightweight n-ary tree data structure for Kotlin Multiplatform (JVM, JS, Wasm, iOS, " + + "Native). DSL, pre/post/level-order traversal, lazy Sequence traversal, and pretty-print.", + ) url.set("https://github.com/AdrianKuta/Tree-Data-Structure") licenses { @@ -54,28 +55,15 @@ mavenPublishing { } } -// No legacy publishing {} block or s01 repos — Central Portal handles it. - repositories { mavenCentral() } -java { - toolchain { - languageVersion.set(JavaLanguageVersion.of(21)) - } -} kotlin { - jvmToolchain(21); - jvm { - compilations.all { - kotlinOptions { - jvmTarget = "21" // <- was "1.8" - } - } - } + jvmToolchain(21) + + jvm() - // JS targets (IR) for publishing js(IR) { browser() nodejs() @@ -87,21 +75,7 @@ kotlin { nodejs() } - rootProject.plugins.withType { - rootProject.extensions.getByType().nodeVersion = "22.0.0" - } - - kotlin.targets.withType { - if (name == "wasmJs") { - @Suppress("UNCHECKED_CAST") - (this as KotlinWasmTargetDsl).apply { - nodejs { - } - } - } - } - - // iOS targets + // Apple targets iosX64() iosArm64() iosSimulatorArm64() @@ -109,7 +83,7 @@ kotlin { // Native host target val hostOs = System.getProperty("os.name") val isMingwX64 = hostOs.startsWith("Windows") - val nativeTarget = when { + when { hostOs == "Mac OS X" -> macosX64("native") hostOs == "Linux" -> linuxX64("native") isMingwX64 -> mingwX64("native") @@ -117,25 +91,8 @@ kotlin { } sourceSets { - val commonMain by getting - val commonTest by getting { dependencies { implementation(kotlin("test")) } } - val jvmMain by getting - val jvmTest by getting - val jsMain by getting - val jsTest by getting - val wasmJsMain by getting - val wasmJsTest by getting - val nativeMain by getting - val nativeTest by getting - - // Shared iOS source sets - val iosMain by creating { dependsOn(commonMain) } - val iosTest by creating { dependsOn(commonTest) } - val iosX64Main by getting { dependsOn(iosMain) } - val iosArm64Main by getting { dependsOn(iosMain) } - val iosSimulatorArm64Main by getting { dependsOn(iosMain) } - val iosX64Test by getting { dependsOn(iosTest) } - val iosArm64Test by getting { dependsOn(iosTest) } - val iosSimulatorArm64Test by getting { dependsOn(iosTest) } + commonTest.dependencies { + implementation(kotlin("test")) + } } } diff --git a/gradle.properties b/gradle.properties index 9dcc3a7..7fc6f1f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1 @@ kotlin.code.style=official -kotlin.js.compiler=ir diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..4526f3e --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,21 @@ +[versions] +kotlin = "2.1.0" +dokka = "1.9.20" +mavenPublish = "0.34.0" +binaryCompatibilityValidator = "0.16.3" +kover = "0.8.3" +coroutines = "1.9.0" +kotlinxSerialization = "1.7.3" + +[plugins] +kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } +kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } +mavenPublish = { id = "com.vanniktech.maven.publish", version.ref = "mavenPublish" } +binaryCompatibilityValidator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binaryCompatibilityValidator" } +kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" } + +[libraries] +kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } +kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerialization" } diff --git a/settings.gradle.kts b/settings.gradle.kts index bc71c01..1d073c0 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,3 +1,4 @@ - rootProject.name = "tree-structure" +include(":tree-structure-serialization") +include(":tree-structure-coroutines") diff --git a/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/TreeNode.kt b/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/TreeNode.kt index 361eb5f..1dd3ec6 100644 --- a/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/TreeNode.kt +++ b/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/TreeNode.kt @@ -9,7 +9,23 @@ import com.github.adriankuta.datastructure.tree.iterators.TreeNodeIterators.* import kotlin.jvm.JvmSynthetic /** - * @param treeIterator Choose one of available iterators from [TreeNodeIterators] + * A node in a generic, mutable n-ary tree. Each node holds a [value], a reference to its [parent] + * and an ordered list of [children]. + * + * Iterating a node (via [iterator], or the lazy [asSequence]/[preOrderSequence] extensions) visits + * the node and all of its descendants. Traversal and the [height]/[nodeCount]/[clear] helpers are + * implemented iteratively, so they are safe on arbitrarily deep trees. + * + * **Not thread-safe.** Nodes are mutable ([addChild]/[removeChild]/[clear] mutate the structure and + * parent pointers). Sharing a tree across threads requires external synchronization, and the tree + * must not be modified while it is being iterated. + * + * Equality is by reference (identity); use the `structurallyEquals` extension to compare two trees + * by value and shape. + * + * @param value the value stored in this node. + * @param treeIterator the default traversal order used by [iterator]. Prefer the + * `asSequence(order)` / `preOrderSequence()` extensions to choose an order without mutating state. */ open class TreeNode(val value: T, var treeIterator: TreeNodeIterators = PreOrder) : Iterable>, ChildDeclarationInterface { diff --git a/tree-structure-coroutines/build.gradle.kts b/tree-structure-coroutines/build.gradle.kts new file mode 100644 index 0000000..73d2407 --- /dev/null +++ b/tree-structure-coroutines/build.gradle.kts @@ -0,0 +1,88 @@ +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl + +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.dokka) + alias(libs.plugins.mavenPublish) + signing +} + +group = "com.github.adriankuta" +version = rootProject.version + +mavenPublishing { + publishToMavenCentral(automaticRelease = false) + signAllPublications() + + coordinates("com.github.adriankuta", "tree-structure-coroutines", version.toString()) + + pom { + name.set("Tree Data Structure — coroutines") + description.set("kotlinx.coroutines Flow traversal for the tree-structure library.") + url.set("https://github.com/AdrianKuta/Tree-Data-Structure") + licenses { + license { + name.set("MIT License") + url.set("https://opensource.org/licenses/MIT") + distribution.set("repo") + } + } + developers { + developer { + id.set("AdrianKuta") + name.set("Adrian Kuta") + email.set("adrian.kuta93@gmail.com") + } + } + scm { + url.set("https://github.com/AdrianKuta/Tree-Data-Structure") + connection.set("scm:git:https://github.com/AdrianKuta/Tree-Data-Structure.git") + developerConnection.set("scm:git:ssh://git@github.com/AdrianKuta/Tree-Data-Structure.git") + } + } +} + +repositories { + mavenCentral() +} + +kotlin { + jvmToolchain(21) + + jvm() + + js(IR) { + browser() + nodejs() + } + + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + browser() + nodejs() + } + + iosX64() + iosArm64() + iosSimulatorArm64() + + val hostOs = System.getProperty("os.name") + val isMingwX64 = hostOs.startsWith("Windows") + when { + hostOs == "Mac OS X" -> macosX64("native") + hostOs == "Linux" -> linuxX64("native") + isMingwX64 -> mingwX64("native") + else -> throw GradleException("Host OS is not supported in Kotlin/Native.") + } + + sourceSets { + commonMain.dependencies { + api(project(":")) + api(libs.kotlinx.coroutines.core) + } + commonTest.dependencies { + implementation(kotlin("test")) + implementation(libs.kotlinx.coroutines.test) + } + } +} diff --git a/tree-structure-coroutines/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/coroutines/TreeNodeFlowExt.kt b/tree-structure-coroutines/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/coroutines/TreeNodeFlowExt.kt new file mode 100644 index 0000000..644c3c0 --- /dev/null +++ b/tree-structure-coroutines/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/coroutines/TreeNodeFlowExt.kt @@ -0,0 +1,24 @@ +package com.github.adriankuta.datastructure.tree.coroutines + +import com.github.adriankuta.datastructure.tree.TreeNode +import com.github.adriankuta.datastructure.tree.asSequence +import com.github.adriankuta.datastructure.tree.iterators.TreeNodeIterators +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.asFlow + +/** + * Emits this node and all of its descendants as a cold [Flow], traversed in the given [order]. + * Useful for plugging tree traversal into coroutine/Flow pipelines (e.g. in a ViewModel). + */ +public fun TreeNode.asFlow( + order: TreeNodeIterators = TreeNodeIterators.PreOrder, +): Flow> = asSequence(order).asFlow() + +/** Pre-order traversal as a cold [Flow]. */ +public fun TreeNode.preOrderFlow(): Flow> = asFlow(TreeNodeIterators.PreOrder) + +/** Post-order traversal as a cold [Flow]. */ +public fun TreeNode.postOrderFlow(): Flow> = asFlow(TreeNodeIterators.PostOrder) + +/** Level-order (breadth-first) traversal as a cold [Flow]. */ +public fun TreeNode.levelOrderFlow(): Flow> = asFlow(TreeNodeIterators.LevelOrder) diff --git a/tree-structure-coroutines/src/commonTest/kotlin/com.github.adriankuta/datastructure/tree/coroutines/TreeNodeFlowTest.kt b/tree-structure-coroutines/src/commonTest/kotlin/com.github.adriankuta/datastructure/tree/coroutines/TreeNodeFlowTest.kt new file mode 100644 index 0000000..6d21da6 --- /dev/null +++ b/tree-structure-coroutines/src/commonTest/kotlin/com.github.adriankuta/datastructure/tree/coroutines/TreeNodeFlowTest.kt @@ -0,0 +1,35 @@ +package com.github.adriankuta.datastructure.tree.coroutines + +import com.github.adriankuta.datastructure.tree.iterators.TreeNodeIterators +import com.github.adriankuta.datastructure.tree.tree +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals + +class TreeNodeFlowTest { + + private fun sample() = tree(1) { + child(2) { + child(4) + child(5) + } + child(3) { + child(6) + } + } + + @Test + fun preOrderFlowEmitsInPreOrder() = runTest { + assertEquals(listOf(1, 2, 4, 5, 3, 6), sample().preOrderFlow().map { it.value }.toList()) + } + + @Test + fun levelOrderFlowEmitsInLevelOrder() = runTest { + assertEquals( + listOf(1, 2, 3, 4, 5, 6), + sample().asFlow(TreeNodeIterators.LevelOrder).map { it.value }.toList(), + ) + } +} diff --git a/tree-structure-serialization/build.gradle.kts b/tree-structure-serialization/build.gradle.kts new file mode 100644 index 0000000..4fa5c3b --- /dev/null +++ b/tree-structure-serialization/build.gradle.kts @@ -0,0 +1,88 @@ +import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl + +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.kotlinSerialization) + alias(libs.plugins.dokka) + alias(libs.plugins.mavenPublish) + signing +} + +group = "com.github.adriankuta" +version = rootProject.version + +mavenPublishing { + publishToMavenCentral(automaticRelease = false) + signAllPublications() + + coordinates("com.github.adriankuta", "tree-structure-serialization", version.toString()) + + pom { + name.set("Tree Data Structure — kotlinx.serialization") + description.set("kotlinx.serialization support (TreeNodeDto round-trip) for the tree-structure library.") + url.set("https://github.com/AdrianKuta/Tree-Data-Structure") + licenses { + license { + name.set("MIT License") + url.set("https://opensource.org/licenses/MIT") + distribution.set("repo") + } + } + developers { + developer { + id.set("AdrianKuta") + name.set("Adrian Kuta") + email.set("adrian.kuta93@gmail.com") + } + } + scm { + url.set("https://github.com/AdrianKuta/Tree-Data-Structure") + connection.set("scm:git:https://github.com/AdrianKuta/Tree-Data-Structure.git") + developerConnection.set("scm:git:ssh://git@github.com/AdrianKuta/Tree-Data-Structure.git") + } + } +} + +repositories { + mavenCentral() +} + +kotlin { + jvmToolchain(21) + + jvm() + + js(IR) { + browser() + nodejs() + } + + @OptIn(ExperimentalWasmDsl::class) + wasmJs { + browser() + nodejs() + } + + iosX64() + iosArm64() + iosSimulatorArm64() + + val hostOs = System.getProperty("os.name") + val isMingwX64 = hostOs.startsWith("Windows") + when { + hostOs == "Mac OS X" -> macosX64("native") + hostOs == "Linux" -> linuxX64("native") + isMingwX64 -> mingwX64("native") + else -> throw GradleException("Host OS is not supported in Kotlin/Native.") + } + + sourceSets { + commonMain.dependencies { + api(project(":")) + api(libs.kotlinx.serialization.json) + } + commonTest.dependencies { + implementation(kotlin("test")) + } + } +} diff --git a/tree-structure-serialization/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/serialization/TreeNodeDto.kt b/tree-structure-serialization/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/serialization/TreeNodeDto.kt new file mode 100644 index 0000000..e5e1aee --- /dev/null +++ b/tree-structure-serialization/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/serialization/TreeNodeDto.kt @@ -0,0 +1,30 @@ +package com.github.adriankuta.datastructure.tree.serialization + +import com.github.adriankuta.datastructure.tree.TreeNode +import kotlinx.serialization.Serializable + +/** + * A serializable, acyclic view of a [TreeNode] subtree. [TreeNode] itself holds a back-reference to + * its parent (a cycle), so it cannot be `@Serializable` directly — convert to/from this DTO instead. + * + * ``` + * val json = Json.encodeToString(tree.toDto()) + * val restored = Json.decodeFromString>(json).toTreeNode() + * ``` + */ +@Serializable +public data class TreeNodeDto( + val value: T, + val children: List> = emptyList(), +) + +/** Converts this subtree into a serializable [TreeNodeDto], preserving values and shape. */ +public fun TreeNode.toDto(): TreeNodeDto = + TreeNodeDto(value, children.map { it.toDto() }) + +/** Rebuilds a mutable [TreeNode] tree from this DTO. */ +public fun TreeNodeDto.toTreeNode(): TreeNode { + val node = TreeNode(value) + children.forEach { node.addChild(it.toTreeNode()) } + return node +} diff --git a/tree-structure-serialization/src/commonTest/kotlin/com.github.adriankuta/datastructure/tree/serialization/TreeNodeSerializationTest.kt b/tree-structure-serialization/src/commonTest/kotlin/com.github.adriankuta/datastructure/tree/serialization/TreeNodeSerializationTest.kt new file mode 100644 index 0000000..8f3e7f6 --- /dev/null +++ b/tree-structure-serialization/src/commonTest/kotlin/com.github.adriankuta/datastructure/tree/serialization/TreeNodeSerializationTest.kt @@ -0,0 +1,40 @@ +package com.github.adriankuta.datastructure.tree.serialization + +import com.github.adriankuta.datastructure.tree.structurallyEquals +import com.github.adriankuta.datastructure.tree.tree +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class TreeNodeSerializationTest { + + @Test + fun roundTripsThroughJson() { + val original = tree("World") { + child("North America") { child("USA") } + child("Europe") { + child("Poland") + child("Germany") + } + } + + val json = Json.encodeToString(original.toDto()) + val restored = Json.decodeFromString>(json).toTreeNode() + + assertTrue(original.structurallyEquals(restored)) + } + + @Test + fun dtoMirrorsTreeShape() { + val dto = tree(1) { + child(2) + child(3) { child(4) } + }.toDto() + + assertEquals(1, dto.value) + assertEquals(2, dto.children.size) + assertEquals(4, dto.children[1].children[0].value) + } +}