15 Commits

Author SHA1 Message Date
Adrian Kuta
1f60b854de Publish API reference (Dokka HTML) to GitHub Pages (#32) (#40)
* docs: design spec for Dokka HTML API reference on GitHub Pages (#32)

* docs: implementation plan for Dokka API reference on GitHub Pages (#32)

* build: migrate Dokka 1.9.20 -> 2.2.0 (DGP v2) (#32)

* docs: aggregate all modules into one Dokka HTML site with source links (#32)

* docs: add Dokka source links to serialization/coroutines/compose modules (#32)

* ci: add docs workflow to deploy Dokka HTML to GitHub Pages (#32)

* docs: link the published API reference from the README (#32)
2026-06-07 21:48:58 +02:00
Adrian Kuta
100054585f chore: ignore the Kotlin 2.x .kotlin/ build cache directory 2026-06-07 19:53:08 +02:00
Adrian Kuta
a7018b8c61 docs: rewrite README for clarity and usefulness
- One consistent example tree throughout; fixed prettyString output mismatch.
- Task-oriented sections (building/traversal/navigation/functional/utilities/mutating).
- Per-module usage for serialization, coroutines, and compose.
- Condensed the maintainer publishing section; added a thread-safety note and CHANGELOG link.
2026-06-07 19:50:02 +02:00
Adrian Kuta
bec1fe02a7 feat: tree-structure-compose (LazyTree) + O(n) addChild cycle check
- New published module tree-structure-compose: a LazyTree composable for Compose
  Multiplatform (JVM/desktop, iOS, Wasm) with lazy rendering and expand/collapse.
- Fix an O(n^2) regression in addChild(): only walk ancestors for cycle detection
  when the child already has a subtree (a fresh leaf can never form a cycle), so
  building deep trees is O(n) again. Caught by the deep-chain stack-safety test on JS.
- README: Compose usage section; align all install snippets to 4.0.0.
- Version catalog: Compose Multiplatform + compose-compiler plugins.

Verified locally: JVM, JS(node), Wasm(node), iOS-simulator tests + apiCheck all green;
Compose module compiles for JVM, Wasm and iOS.
2026-06-07 18:55:07 +02:00
Adrian Kuta
69d19f89e3 feat!: v4.0 breaking API cleanup + explicitApi
BREAKING changes to the core:
- treeIterator is now a read-only `val`; added `iterator(order)` and use `asSequence(order)`.
- removeChild() only removes a direct child of the receiver; added `detach()` to unhook a node.
- addChild() rejects re-parenting and cycles (throws TreeNodeException); detach() first to move.
- clear() no longer nulls the receiver's own parent; only removes descendants.
- path() returns List<TreeNode<T>>? (null) instead of throwing.

Also:
- Enable strict explicitApi() across core + both modules; add explicit `public` modifiers.
- Update tests for the new contracts + add TreeNodeV4Test; refresh .api baselines.
- README + CHANGELOG (with migration notes); bump version to 4.0.0.

47 JVM tests green.
2026-06-07 18:47:40 +02:00
Adrian Kuta
c9bbea59b0 ci: binary-compat API baselines, multiplatform matrix CI, module docs
- Commit binary-compatibility-validator .api baselines for core + both modules.
- Replace JVM-only CI with a matrix: Ubuntu runs JVM/JS(node)/Wasm(node)/Native
  + apiCheck; macOS runs the iOS simulator tests.
- README: install + usage for tree-structure-serialization and -coroutines.
- Update kotlin-js-store/yarn.lock for the new modules' JS test deps.

Verified locally: JVM, JS(node), Wasm(node) and iOS-simulator tests pass for
core + both modules; native compiles & links (exec runs on the Linux CI runner).
2026-06-06 13:51:22 +02:00
Adrian Kuta
e1f01c4e2d 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).
2026-06-06 13:47:20 +02:00
Adrian Kuta
c45c5b7afa feat: stack-safe traversal + lazy Sequence + navigation/functional extensions
Core additive work for v3.4 (non-breaking):

- Rewrite nodeCount(), height(), clear() and the post-order iterator iteratively
  so deep/degenerate trees no longer throw StackOverflowError (verified to 50k deep).
- Add lazy Sequence traversal: asSequence(order), pre/post/levelOrderSequence().
- Add navigation extensions: isLeaf, degree, root(), ancestors(), siblings(),
  leaves(), descendants().
- Add functional extensions: findNode, filterNodes, anyNode, allNodes, countNodes,
  foldNodes, mapValues, deepCopy, structurallyEquals (all stack-safe).
- Add tests for stack-safety, the new APIs, and previously-uncovered
  height/depth/nodeCount/path (incl. exception paths). 40 tests green on JVM.
2026-06-06 13:35:39 +02:00
Adrian Kuta
6de95f7976 Release 3.1.5: remove debug println, drop worksheet leftover, modernize CI
- Remove stray println(child.value) from TreeNode.removeChild()
- Add regression test for removeChild() return value
- Delete leftover ExampleUnitTest.kt template test
- Remove Example.ws.kts worksheet and kotlin("script-runtime") from jvmMain
- Bump actions/checkout v2 -> v4 in CI workflows
- Update README install snippets to 3.1.5
- Bump version to 3.1.5
2026-06-06 13:03:00 +02:00
Adrian Kuta
ea0b4fa95c Bump version to 3.1.4 2025-12-25 22:53:43 +01:00
Adrian Kuta
9d52b152bd Merge pull request #31 from AdrianKuta/develop
Develop
2025-12-25 22:46:09 +01:00
Adrian Kuta
0fc7b16664 feat: Update Kotlin and JS dependencies and add Wasm target (#30)
- Upgraded Kotlin from `1.9.20` to `1.9.24`.
- Added a new Kotlin/Wasm target (`wasmJs`) for both browser and Node.js environments.
- Updated numerous JavaScript dependencies in `yarn.lock`, including major upgrades for `webpack`, `terser`, and various `@webpack-cli`, `@jridgewell`, and `@webassemblyjs` packages.
- Configured the Node.js version to `22.0.0` for JS and Wasm targets.
2025-12-25 22:43:29 +01:00
Adrian Kuta
d3086a5ced chore: Remove Dependabot configuration
Removes the `.github/dependabot.yml` file, disabling the Dependabot version update checks for the project.
2025-12-25 22:43:17 +01:00
Adrian Kuta
2071084964 Bump version to 3.1.3 2025-12-25 22:41:48 +01:00
Adrian Kuta
f51964831a feat: Ignore .idea directory
- Adds the entire `.idea/` directory to `.gitignore` to prevent IDE-specific files from being committed.
2025-12-25 22:29:06 +01:00
56 changed files with 2942 additions and 958 deletions

View File

@@ -1,11 +0,0 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: "" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"

46
.github/workflows/docs.yml vendored Normal file
View File

@@ -0,0 +1,46 @@
name: Docs
on:
release:
types: [released]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: false
jobs:
build:
name: Build Dokka HTML
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
- name: Generate API docs
run: ./gradlew :dokkaGeneratePublicationHtml --console=plain
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v3
with:
path: build/dokka/html
deploy:
name: Deploy to GitHub Pages
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy
id: deployment
uses: actions/deploy-pages@v4

View File

@@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:

View File

@@ -6,18 +6,26 @@ on:
workflow_call:
jobs:
build:
name: Run unit tests
runs-on: ubuntu-latest
test:
name: ${{ matrix.name }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- name: JVM / JS / Wasm / Native + API check
os: ubuntu-latest
tasks: jvmTest jsNodeTest wasmJsNodeTest nativeTest apiCheck
- name: iOS
os: macos-latest
tasks: iosSimulatorArm64Test
steps:
- name: Check out code
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
# Builds the release artifacts of the library
- name: Test
run: ./gradlew cleanJvmTest jvmTest
run: ./gradlew ${{ matrix.tasks }} --console=plain

3
.gitignore vendored
View File

@@ -1,5 +1,6 @@
*.iml
.gradle
/.kotlin/
/local.properties
/.idea/caches
/.idea/libraries
@@ -9,6 +10,7 @@
/.idea/assetWizardSettings.xml
.DS_Store
/build
build/
/captures
.externalNativeBuild
.cxx
@@ -16,3 +18,4 @@
/.idea/compiler.xml
/.idea/jarRepositories.xml
/.idea/deploymentTargetDropDown.xml
/.idea/

4
.idea/.gitignore generated vendored
View File

@@ -1,4 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
artifacts

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AndroidProjectSystem">
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
</component>
</project>

View File

@@ -1,45 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JavaCodeStyleSettings>
<option name="IMPORT_LAYOUT_TABLE">
<value>
<package name="" withSubpackages="true" static="false" module="true" />
<package name="android" withSubpackages="true" static="true" />
<package name="androidx" withSubpackages="true" static="true" />
<package name="com" withSubpackages="true" static="true" />
<package name="junit" withSubpackages="true" static="true" />
<package name="net" withSubpackages="true" static="true" />
<package name="org" withSubpackages="true" static="true" />
<package name="java" withSubpackages="true" static="true" />
<package name="javax" withSubpackages="true" static="true" />
<package name="" withSubpackages="true" static="true" />
<emptyLine />
<package name="android" withSubpackages="true" static="false" />
<emptyLine />
<package name="androidx" withSubpackages="true" static="false" />
<emptyLine />
<package name="com" withSubpackages="true" static="false" />
<emptyLine />
<package name="junit" withSubpackages="true" static="false" />
<emptyLine />
<package name="net" withSubpackages="true" static="false" />
<emptyLine />
<package name="org" withSubpackages="true" static="false" />
<emptyLine />
<package name="java" withSubpackages="true" static="false" />
<emptyLine />
<package name="javax" withSubpackages="true" static="false" />
<emptyLine />
<package name="" withSubpackages="true" static="false" />
<emptyLine />
</value>
</option>
</JavaCodeStyleSettings>
<JetCodeStyleSettings>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</codeStyleSettings>
</code_scheme>
</component>

View File

@@ -1,5 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

18
.idea/gradle.xml generated
View File

@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>

View File

@@ -1,6 +0,0 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="ReplaceUntilWithRangeUntil" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
</profile>
</component>

7
.idea/misc.xml generated
View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

View File

@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
</set>
</option>
</component>
</project>

6
.idea/studiobot.xml generated
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="StudioBotProjectSettings">
<option name="shareContext" value="OptedIn" />
</component>
</project>

6
.idea/vcs.xml generated
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

84
CHANGELOG.md Normal file
View File

@@ -0,0 +1,84 @@
# 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]
### Changed
- Rewrote the README for clarity: one consistent example tree, task-oriented sections
(building, traversal, navigation, functional, utilities, mutating), per-module usage, and a
condensed maintainer "Releasing" section.
## [4.0.0]
A breaking release that cleans up the core API and enforces an explicit public surface.
### Changed (breaking)
- `TreeNode.treeIterator` is now a read-only `val` (set it via the constructor). Use
`iterator(order)` or `asSequence(order)` to traverse in a different order per call.
- `removeChild(child)` now only removes a **direct** child of the receiver (previously it removed
the node from its actual parent regardless). Use `child.detach()` to unhook a node from wherever
it lives.
- `addChild(child)` now throws `TreeNodeException` if `child` already has a parent or if the
attachment would create a cycle. Call `detach()` first to move a node.
- `clear()` no longer detaches the receiver from its own parent; it only removes its descendants.
- `path(descendant)` now returns `List<TreeNode<T>>?` (`null` when `descendant` is the root or not a
descendant) instead of throwing `TreeNodeException`.
### Added
- `TreeNode.detach()` — removes a node from its parent.
- `TreeNode.iterator(order)` — a one-shot iterator in a specific order.
- Strict `explicitApi()` mode across all modules.
- New `tree-structure-compose` module: a `LazyTree` composable for Compose Multiplatform.
### Migration
- `node.treeIterator = PostOrder; for (n in node) { … }``for (n in node.asSequence(PostOrder)) { … }`
- `root.removeChild(deepNode)``deepNode.detach()`
- `try { node.path(x) } catch (e: TreeNodeException) { … }``node.path(x)?.let { … }`
## [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/v4.0.0...HEAD
[4.0.0]: https://github.com/AdrianKuta/Tree-Data-Structure/compare/v3.4.0...v4.0.0
[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

241
README.md
View File

@@ -2,13 +2,27 @@
[![maven](https://img.shields.io/maven-central/v/com.github.adriankuta/tree-structure?style=plastic)](https://mvnrepository.com/artifact/com.github.adriankuta/tree-structure)
[![License: MIT](https://img.shields.io/github/license/AdrianKuta/Tree-Data-Structure?style=plastic)](https://github.com/AdrianKuta/Tree-Data-Structure/blob/master/LICENSE)
[![Publish](https://github.com/AdrianKuta/Tree-Data-Structure/actions/workflows/publishRelease.yml/badge.svg)](https://github.com/AdrianKuta/Tree-Data-Structure/actions/workflows/publishRelease.yml)
[![API docs](https://img.shields.io/badge/docs-API%20reference-blue?style=plastic)](https://adriankuta.github.io/Tree-Data-Structure/)
Lightweight Kotlin Multiplatform tree data structure for Kotlin and Java. Includes a small DSL, multiple traversal iterators, and pretty-print support.
📖 **[API reference](https://adriankuta.github.io/Tree-Data-Structure/)** — full KDoc for the core and all modules.
- Kotlin Multiplatform (JVM, JS, iOS, and Native host)
- Pre-order, Post-order, and Level-order iteration
- Simple DSL: tree { child(...) }
- Utilities: nodeCount(), height(), depth(), path(), prettyString(), clear(), removeChild()
A lightweight n-ary tree for Kotlin Multiplatform. You get a generic `TreeNode<T>`, a small DSL for
building trees, three traversal orders, lazy `Sequence` traversal, and a set of navigation and
functional helpers. The core artifact has no third-party dependencies.
It fits homogeneous trees of arbitrary depth: UI component hierarchies, file-system views, org
charts, and category menus. For fixed, typed hierarchies (like a compiler AST) a sealed class is
usually a better fit.
## Features
- Kotlin Multiplatform: JVM, JS, Wasm, iOS, and a native host target.
- Build trees with a `tree { child(...) }` DSL or node by node with `addChild`.
- Pre-order, post-order, and level-order traversal, as iterators or lazy `Sequence`s.
- Navigation: `root()`, `ancestors()`, `siblings()`, `leaves()`, `descendants()`, `isLeaf`, `degree`.
- Functional helpers: `findNode`, `filterNodes`, `anyNode`, `allNodes`, `foldNodes`, `mapValues`, `deepCopy`, `structurallyEquals`.
- Utilities: `nodeCount()`, `height()`, `depth()`, `path()`, `prettyString()`.
- Stack-safe: traversal and `height()`/`nodeCount()`/`clear()` handle very deep trees without `StackOverflowError`.
## Installation
@@ -16,14 +30,14 @@ Gradle (Kotlin DSL):
```kotlin
// commonMain for KMP projects, or any sourceSet/module where you need it
dependencies {
implementation("com.github.adriankuta:tree-structure:3.1.1") // see badge above for the latest version
implementation("com.github.adriankuta:tree-structure:4.0.0") // latest version is on the badge above
}
```
Gradle (Groovy):
```groovy
dependencies {
implementation "com.github.adriankuta:tree-structure:3.1.1" // see badge above for the latest
implementation "com.github.adriankuta:tree-structure:4.0.0"
}
```
@@ -32,31 +46,13 @@ Maven:
<dependency>
<groupId>com.github.adriankuta</groupId>
<artifactId>tree-structure</artifactId>
<version>3.1.1</version>
<version>4.0.0</version>
</dependency>
```
## Usage
## Building a tree
**Kotlin**
```kotlin
val root = TreeNode("World")
val northA = TreeNode("North America")
val europe = TreeNode("Europe")
root.addChild(northA)
root.addChild(europe)
val usa = TreeNode("USA")
northA.addChild(usa)
val poland = TreeNode("Poland")
val france = TreeNode("France")
europe.addChild(poland)
europe.addChild(france)
println(root.prettyString())
```
**Pretty Kotlin (DSL)**
The DSL is the shortest way to build one:
```kotlin
val root = tree("World") {
child("North America") { child("USA") }
@@ -67,86 +63,157 @@ val root = tree("World") {
}
```
**Java**
The same node-by-node API works from Kotlin and Java:
```java
TreeNode<String> root = new TreeNode<>("World");
TreeNode<String> northA = new TreeNode<>("North America");
TreeNode<String> northAmerica = new TreeNode<>("North America");
root.addChild(northAmerica);
northAmerica.addChild(new TreeNode<>("USA"));
TreeNode<String> europe = new TreeNode<>("Europe");
root.addChild(northA);
root.addChild(europe);
TreeNode<String> usa = new TreeNode<>("USA");
northA.addChild(usa);
TreeNode<String> poland = new TreeNode<>("Poland");
TreeNode<String> france = new TreeNode<>("France");
europe.addChild(poland);
europe.addChild(france);
System.out.println(root.prettyString());
europe.addChild(new TreeNode<>("Poland"));
europe.addChild(new TreeNode<>("Germany"));
```
Output:
`prettyString()` renders the tree for logs and debugging:
```
World
├── North America
│ └── USA
└── Europe
├── Poland
└── France
└── Germany
```
### Traversal and utilities
## Traversal
Iterating a node visits the node and all of its descendants. The default order is set in the
constructor (pre-order by default) and is read-only. Pass an order per call when you need a
different one:
```kotlin
val root = TreeNode("root")
// ... build your tree
// Choose iteration order (default is PreOrder)
root.treeIterator = TreeNodeIterators.PostOrder
for (node in root) println(node.value)
// Utilities
root.nodeCount() // number of descendants
root.height() // longest path to a leaf (in edges)
root.depth() // distance from current node to the root
val path = root.path(root.children.first()) // nodes from descendant up to root
// Mutations
val child = root.children.first()
root.removeChild(child)
root.clear() // remove entire subtree
for (node in root) println(node.value) // default pre-order
for (node in root.asSequence(TreeNodeIterators.PostOrder)) println(node.value)
```
## Publishing to Maven Central (central.sonatype.com)
Traversal is also exposed as a lazy `Sequence`, so it composes with the standard library and stops
early instead of materializing the whole tree:
```kotlin
root.preOrderSequence().map { it.value }.toList() // [World, North America, USA, Europe, Poland, Germany]
root.levelOrderSequence().first { it.value == "USA" } // stops as soon as it is found
root.asSequence(TreeNodeIterators.PostOrder).count() // 6
```
This project is configured to publish artifacts to Maven Central via the Sonatype Central Portal.
## Navigation
```kotlin
val usa = root.findNode { it == "USA" }!!
There are two supported ways to publish:
usa.isLeaf // true
usa.depth() // 2
usa.root().value // "World"
usa.ancestors().map { it.value } // [North America, World]
root.leaves().map { it.value } // [USA, Poland, Germany]
```
1) Via GitHub Actions (recommended)
- Create a GitHub Release (tag) in this repository. When a release is published, the workflow .github/workflows/publishRelease.yml runs automatically.
- The workflow uses the Gradle task publishToMavenCentral to upload artifacts through the Central Portal.
- Make sure these repository secrets are configured in GitHub:
- MAVEN_CENTRAL_USERNAME — Your Sonatype Central username (not email).
- MAVEN_CENTRAL_PASSWORD — Your Sonatype Central password or a token from central.sonatype.com.
- SIGNING_KEY — ASCIIarmored GPG private key (exported, single line; for inmemory signing).
- SIGNING_PASSWORD — Passphrase for the key above.
- The workflow uses JDK 21 and publishes the version defined in build.gradle.kts.
## Functional operations
```kotlin
root.anyNode { it == "Poland" } // true
root.filterNodes { it.length > 5 } // nodes whose value is longer than 5 characters
root.countNodes { it.startsWith("U") } // 1
2) Locally via Gradle
- Ensure you have a Sonatype Central account and that the groupId com.github.adriankuta is verified in central.sonatype.com (Namespace Rules → Verify).
- Export the same credentials/signing values as environment variables or pass them as Gradle properties:
- ORG_GRADLE_PROJECT_mavenCentralUsername
- ORG_GRADLE_PROJECT_mavenCentralPassword
- ORG_GRADLE_PROJECT_signingInMemoryKey
- ORG_GRADLE_PROJECT_signingInMemoryKeyPassword
- Then run:
- ./gradlew publishToMavenCentral
- For snapshot publishing, set -Psnapshot=true (the version is derived from PUBLISH_VERSION with -SNAPSHOT).
val lengths: TreeNode<Int> = root.mapValues { it.length } // a new tree; the original is untouched
val copy = root.deepCopy()
root.structurallyEquals(copy) // true: same values and shape, different nodes
```
Notes
- Publishing is powered by the com.vanniktech.maven.publish Gradle plugin and Sonatype Central Portal (no legacy Nexus staging URLs needed).
- The plugin is configured to sign all publications. Coordinates and POM metadata are defined in build.gradle.kts.
- If using the combined task is preferred, you can also run publishAndReleaseToMavenCentral when automatic release is enabled; this repository currently uploads with publishToMavenCentral from CI.
## Utilities
```kotlin
root.nodeCount() // number of descendants, excluding the root
root.height() // edges on the longest path down to a leaf
root.depth() // edges from this node up to the root
root.path(usa) // [USA, North America, World], or null if usa is not a descendant
```
## Mutating a tree
```kotlin
// addChild rejects a node that already has a parent or that would create a cycle.
root.addChild(TreeNode("Asia"))
// removeChild removes a direct child of the receiver and returns true if it was present.
root.removeChild(root.children.first())
// detach() unhooks a node from wherever it currently lives.
root.findNode { it == "Germany" }?.detach()
// clear() removes every descendant of the node.
root.clear()
```
## Optional modules
The core artifact has no third-party dependencies. Each integration is a separate, opt-in artifact
that depends on the core.
### Serialization (`tree-structure-serialization`)
`kotlinx.serialization` support. A `TreeNode` keeps a reference back to its parent, so it cannot be
`@Serializable` directly. Convert to and from the acyclic `TreeNodeDto` instead.
```kotlin
implementation("com.github.adriankuta:tree-structure-serialization:4.0.0")
```
```kotlin
val json = Json.encodeToString(root.toDto())
val restored = Json.decodeFromString<TreeNodeDto<String>>(json).toTreeNode()
```
### Coroutines (`tree-structure-coroutines`)
Traverse a tree as a cold `Flow`, which is handy inside coroutine and `ViewModel` pipelines.
```kotlin
implementation("com.github.adriankuta:tree-structure-coroutines:4.0.0")
```
```kotlin
root.preOrderFlow().collect { println(it.value) }
root.asFlow(TreeNodeIterators.LevelOrder).map { it.value }
```
### Compose UI (`tree-structure-compose`)
A `LazyTree` composable for Compose Multiplatform (JVM/desktop, iOS, Wasm). Only the visible nodes
are composed, and you decide how each node looks:
```kotlin
implementation("com.github.adriankuta:tree-structure-compose:4.0.0")
```
```kotlin
LazyTree(root) { node, depth, expanded, toggle ->
Row(Modifier.padding(start = (depth * 16).dp).clickable(onClick = toggle)) {
if (!node.isLeaf) Text(if (expanded) "" else "")
Text(node.value.toString())
}
}
```
## Notes
`TreeNode` is mutable and not thread-safe. Add your own synchronization if you share a tree across
threads, and do not modify a tree while you iterate it. Equality is by reference; use
`structurallyEquals` to compare two trees by value and shape.
Coming from 3.x? See [CHANGELOG.md](CHANGELOG.md) for the 4.0 migration notes.
## Releasing (maintainers)
Releases go to Maven Central through the Sonatype Central Portal using the
`com.vanniktech.maven.publish` plugin. Creating a GitHub release runs
`.github/workflows/publishRelease.yml`, which signs and uploads every module; the deployment is then
published from central.sonatype.com. The published version comes from `PUBLISH_VERSION` in
`build.gradle.kts`. CI needs the `MAVEN_CENTRAL_USERNAME`, `MAVEN_CENTRAL_PASSWORD`, `SIGNING_KEY`,
and `SIGNING_PASSWORD` repository secrets. To publish from a local machine, set the matching
`ORG_GRADLE_PROJECT_*` properties and run `./gradlew publishToMavenCentral` (add `-Psnapshot=true`
for a snapshot build).
## License
@@ -171,5 +238,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---

101
api/tree-structure.api Normal file
View File

@@ -0,0 +1,101 @@
public abstract interface class com/github/adriankuta/datastructure/tree/ChildDeclarationInterface {
public abstract synthetic fun child (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)Lcom/github/adriankuta/datastructure/tree/TreeNode;
}
public final class com/github/adriankuta/datastructure/tree/ChildDeclarationInterface$DefaultImpls {
public static synthetic fun child$default (Lcom/github/adriankuta/datastructure/tree/ChildDeclarationInterface;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/github/adriankuta/datastructure/tree/TreeNode;
}
public class com/github/adriankuta/datastructure/tree/TreeNode : com/github/adriankuta/datastructure/tree/ChildDeclarationInterface, java/lang/Iterable, kotlin/jvm/internal/markers/KMappedMarker {
public fun <init> (Ljava/lang/Object;Lcom/github/adriankuta/datastructure/tree/iterators/TreeNodeIterators;)V
public synthetic fun <init> (Ljava/lang/Object;Lcom/github/adriankuta/datastructure/tree/iterators/TreeNodeIterators;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun addChild (Lcom/github/adriankuta/datastructure/tree/TreeNode;)V
public synthetic fun child (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)Lcom/github/adriankuta/datastructure/tree/TreeNode;
public final fun clear ()V
public final fun depth ()I
public final fun detach ()Z
public final fun getChildren ()Ljava/util/List;
public final fun getParent ()Lcom/github/adriankuta/datastructure/tree/TreeNode;
public final fun getTreeIterator ()Lcom/github/adriankuta/datastructure/tree/iterators/TreeNodeIterators;
public final fun getValue ()Ljava/lang/Object;
public final fun height ()I
public final fun isRoot ()Z
public fun iterator ()Ljava/util/Iterator;
public final fun iterator (Lcom/github/adriankuta/datastructure/tree/iterators/TreeNodeIterators;)Ljava/util/Iterator;
public final fun nodeCount ()I
public final fun path (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Ljava/util/List;
public final fun prettyString ()Ljava/lang/String;
public final fun removeChild (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Z
public fun toString ()Ljava/lang/String;
}
public final class com/github/adriankuta/datastructure/tree/TreeNodeFunctionalExtKt {
public static final fun allNodes (Lcom/github/adriankuta/datastructure/tree/TreeNode;Lkotlin/jvm/functions/Function1;)Z
public static final fun anyNode (Lcom/github/adriankuta/datastructure/tree/TreeNode;Lkotlin/jvm/functions/Function1;)Z
public static final fun countNodes (Lcom/github/adriankuta/datastructure/tree/TreeNode;Lkotlin/jvm/functions/Function1;)I
public static final fun deepCopy (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Lcom/github/adriankuta/datastructure/tree/TreeNode;
public static final fun filterNodes (Lcom/github/adriankuta/datastructure/tree/TreeNode;Lkotlin/jvm/functions/Function1;)Ljava/util/List;
public static final fun findNode (Lcom/github/adriankuta/datastructure/tree/TreeNode;Lkotlin/jvm/functions/Function1;)Lcom/github/adriankuta/datastructure/tree/TreeNode;
public static final fun foldNodes (Lcom/github/adriankuta/datastructure/tree/TreeNode;Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object;
public static final fun mapValues (Lcom/github/adriankuta/datastructure/tree/TreeNode;Lkotlin/jvm/functions/Function1;)Lcom/github/adriankuta/datastructure/tree/TreeNode;
public static final fun structurallyEquals (Lcom/github/adriankuta/datastructure/tree/TreeNode;Lcom/github/adriankuta/datastructure/tree/TreeNode;)Z
}
public final class com/github/adriankuta/datastructure/tree/TreeNodeNavigationExtKt {
public static final fun ancestors (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Ljava/util/List;
public static final fun descendants (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Ljava/util/List;
public static final fun getDegree (Lcom/github/adriankuta/datastructure/tree/TreeNode;)I
public static final fun isLeaf (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Z
public static final fun leaves (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Ljava/util/List;
public static final fun root (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Lcom/github/adriankuta/datastructure/tree/TreeNode;
public static final fun siblings (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Ljava/util/List;
}
public final class com/github/adriankuta/datastructure/tree/TreeNodeSequenceExtKt {
public static final fun asSequence (Lcom/github/adriankuta/datastructure/tree/TreeNode;Lcom/github/adriankuta/datastructure/tree/iterators/TreeNodeIterators;)Lkotlin/sequences/Sequence;
public static synthetic fun asSequence$default (Lcom/github/adriankuta/datastructure/tree/TreeNode;Lcom/github/adriankuta/datastructure/tree/iterators/TreeNodeIterators;ILjava/lang/Object;)Lkotlin/sequences/Sequence;
public static final fun levelOrderSequence (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Lkotlin/sequences/Sequence;
public static final fun postOrderSequence (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Lkotlin/sequences/Sequence;
public static final fun preOrderSequence (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Lkotlin/sequences/Sequence;
}
public final class com/github/adriankuta/datastructure/tree/exceptions/TreeNodeException : java/lang/RuntimeException {
public fun <init> ()V
public fun <init> (Ljava/lang/String;)V
public fun <init> (Ljava/lang/String;Ljava/lang/Throwable;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
}
public final class com/github/adriankuta/datastructure/tree/iterators/LevelOrderTreeIterator : java/util/Iterator, kotlin/jvm/internal/markers/KMappedMarker {
public fun <init> (Lcom/github/adriankuta/datastructure/tree/TreeNode;)V
public fun hasNext ()Z
public fun next ()Lcom/github/adriankuta/datastructure/tree/TreeNode;
public synthetic fun next ()Ljava/lang/Object;
public fun remove ()V
}
public final class com/github/adriankuta/datastructure/tree/iterators/PostOrderTreeIterator : java/util/Iterator, kotlin/jvm/internal/markers/KMappedMarker {
public fun <init> (Lcom/github/adriankuta/datastructure/tree/TreeNode;)V
public fun hasNext ()Z
public fun next ()Lcom/github/adriankuta/datastructure/tree/TreeNode;
public synthetic fun next ()Ljava/lang/Object;
public fun remove ()V
}
public final class com/github/adriankuta/datastructure/tree/iterators/PreOrderTreeIterator : java/util/Iterator, kotlin/jvm/internal/markers/KMappedMarker {
public fun <init> (Lcom/github/adriankuta/datastructure/tree/TreeNode;)V
public fun hasNext ()Z
public fun next ()Lcom/github/adriankuta/datastructure/tree/TreeNode;
public synthetic fun next ()Ljava/lang/Object;
public fun remove ()V
}
public final class com/github/adriankuta/datastructure/tree/iterators/TreeNodeIterators : java/lang/Enum {
public static final field LevelOrder Lcom/github/adriankuta/datastructure/tree/iterators/TreeNodeIterators;
public static final field PostOrder Lcom/github/adriankuta/datastructure/tree/iterators/TreeNodeIterators;
public static final field PreOrder Lcom/github/adriankuta/datastructure/tree/iterators/TreeNodeIterators;
public static fun getEntries ()Lkotlin/enums/EnumEntries;
public static fun valueOf (Ljava/lang/String;)Lcom/github/adriankuta/datastructure/tree/iterators/TreeNodeIterators;
public static fun values ()[Lcom/github/adriankuta/datastructure/tree/iterators/TreeNodeIterators;
}

View File

@@ -1,15 +1,17 @@
import org.jetbrains.kotlin.konan.properties.Properties
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
plugins {
kotlin("multiplatform") version "1.9.20"
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.2"
val PUBLISH_VERSION = "4.0.0"
val snapshot: String? by project
@@ -25,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 {
@@ -50,34 +55,51 @@ mavenPublishing {
}
}
// No legacy publishing {} block or s01 repos — Central Portal handles it.
repositories {
mavenCentral()
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(21))
}
dependencies {
// Include this module's own docs in the aggregation — DGP v2 requires the
// aggregating project to list itself explicitly.
dokka(project(":"))
dokka(project(":tree-structure-serialization"))
dokka(project(":tree-structure-coroutines"))
dokka(project(":tree-structure-compose"))
}
kotlin {
jvmToolchain(21);
jvm {
compilations.all {
kotlinOptions {
jvmTarget = "21" // <- was "1.8"
}
dokka {
moduleName.set("Tree Data Structure")
dokkaSourceSets.configureEach {
sourceLink {
localDirectory.set(projectDir.resolve("src"))
// For the root project projectDir == rootDir, so `module` is "" and links resolve to /blob/master/src.
val module = projectDir.relativeTo(rootDir).invariantSeparatorsPath
val prefix = if (module.isEmpty()) "" else "$module/"
remoteUrl("https://github.com/AdrianKuta/Tree-Data-Structure/blob/master/${prefix}src")
remoteLineSuffix.set("#L")
}
}
}
kotlin {
explicitApi()
jvmToolchain(21)
jvm()
// JS targets (IR) for publishing
js(IR) {
browser()
nodejs()
}
// iOS targets
@OptIn(ExperimentalWasmDsl::class)
wasmJs {
browser()
nodejs()
}
// Apple targets
iosX64()
iosArm64()
iosSimulatorArm64()
@@ -85,7 +107,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")
@@ -93,23 +115,8 @@ kotlin {
}
sourceSets {
val commonMain by getting
val commonTest by getting { dependencies { implementation(kotlin("test")) } }
val jvmMain by getting { dependencies { implementation(kotlin("script-runtime")) } }
val jvmTest by getting
val jsMain by getting
val jsTest 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"))
}
}
}

View File

@@ -0,0 +1,435 @@
# API Reference (Dokka HTML) on GitHub Pages — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Migrate Dokka to 2.x and publish an aggregated, source-linked multi-module API reference for all four modules to GitHub Pages on each release and on demand.
**Architecture:** Bump Dokka `1.9.20 → 2.2.0` and switch on the Dokka Gradle Plugin v2 (`V2Enabled`). The root project (the published core module) aggregates the three submodules via `dokka(project(...))` dependencies, producing one HTML site at `build/dokka/html`. A new `docs.yml` workflow builds that site and deploys it with the official GitHub Pages Actions. The vanniktech maven-publish plugin (0.34.0) keeps building the Maven Central `-javadoc.jar` — it already supports Dokka V2, so the release pipeline is unaffected.
**Tech Stack:** Kotlin Multiplatform 2.1.0, Gradle 8.5, Dokka Gradle Plugin 2.2.0, vanniktech maven-publish 0.34.0, GitHub Actions (`upload-pages-artifact@v3`, `deploy-pages@v4`).
**Spec:** `docs/superpowers/specs/2026-06-07-publish-api-reference-dokka-github-pages-design.md`
> **Note on "tests":** This is build/CI work, so each task's verification is a Gradle command or a YAML lint with a concrete expected result rather than a unit test. Treat the "verify" steps as the failing/passing check.
---
## File Structure
| File | Responsibility | Action |
| --- | --- | --- |
| `gradle/libs.versions.toml` | Centralized Dokka version | Modify (`dokka = "2.2.0"`) |
| `gradle.properties` | Enable DGP v2 plugin mode | Modify (add 2 flags) |
| `build.gradle.kts` (root) | Aggregate 3 submodules; root site title + source links | Modify (add `dependencies` + `dokka {}` blocks) |
| `tree-structure-serialization/build.gradle.kts` | Source links for this module | Modify (add `dokka {}` block) |
| `tree-structure-coroutines/build.gradle.kts` | Source links for this module | Modify (add `dokka {}` block) |
| `tree-structure-compose/build.gradle.kts` | Source links for this module | Modify (add `dokka {}` block) |
| `.github/workflows/docs.yml` | Build + deploy site to Pages | Create |
| `README.md` | Docs badge + link | Modify |
Work happens on branch `docs/api-reference-github-pages` (already created; spec already committed there).
---
## Task 1: Migrate Dokka to 2.2.0 and enable DGP v2
**Files:**
- Modify: `gradle/libs.versions.toml` (line `dokka = "1.9.20"`)
- Modify: `gradle.properties`
- [ ] **Step 1: Bump the Dokka version in the catalog**
In `gradle/libs.versions.toml`, change the `dokka` version under `[versions]`:
```toml
dokka = "2.2.0"
```
(Leave the `dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" }` plugin line unchanged.)
- [ ] **Step 2: Enable the Dokka Gradle Plugin v2**
Append these two lines to `gradle.properties` (current content is only `kotlin.code.style=official`):
```properties
# Dokka Gradle Plugin v2 (https://kotl.in/dokka-gradle-migration)
org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled
org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true
```
- [ ] **Step 3: Verify the v2 task exists and there is no V1 warning**
Run: `./gradlew :dokkaGeneratePublicationHtml --dry-run --console=plain`
Expected: `BUILD SUCCESSFUL`, a list of `:dokkaGenerate*` tasks printed, and **no** message containing `Dokka Gradle plugin V1` or `migration guide`. (At this point only the root/core module is documented — aggregation is added in Task 2.)
- [ ] **Step 4: Verify the Maven Central javadoc jar still builds under Dokka 2.x**
Run: `./gradlew javadocJar --console=plain`
Expected: `BUILD SUCCESSFUL`. This is the vanniktech-generated jar that Maven Central requires; it must keep working. (If the task name isn't found, list it with `./gradlew tasks --all | grep -i javadoc` and run the reported task — it is the per-module `javadocJar`.)
- [ ] **Step 5: Commit**
```bash
git add gradle/libs.versions.toml gradle.properties
git commit -m "build: migrate Dokka 1.9.20 -> 2.2.0 (DGP v2) (#32)"
```
---
## Task 2: Aggregate all modules + root site title and source links
**Files:**
- Modify: `build.gradle.kts` (root)
- [ ] **Step 1: Add the Dokka aggregation dependencies**
Add this top-level block to the root `build.gradle.kts` (place it after the `repositories { mavenCentral() }` block, at the top level — not inside `kotlin {}`):
```kotlin
dependencies {
dokka(project(":tree-structure-serialization"))
dokka(project(":tree-structure-coroutines"))
dokka(project(":tree-structure-compose"))
}
```
- [ ] **Step 2: Add the root `dokka {}` configuration (site title + source links)**
Add this top-level block to the root `build.gradle.kts` (e.g. right after the `dependencies {}` block from Step 1):
```kotlin
dokka {
moduleName.set("Tree Data Structure")
dokkaSourceSets.configureEach {
sourceLink {
localDirectory.set(projectDir.resolve("src"))
val module = projectDir.relativeTo(rootDir).invariantSeparatorsPath
val prefix = if (module.isEmpty()) "" else "$module/"
remoteUrl("https://github.com/AdrianKuta/Tree-Data-Structure/blob/master/${prefix}src")
remoteLineSuffix.set("#L")
}
}
}
```
- [ ] **Step 3: Build the aggregated site**
Run: `./gradlew :dokkaGeneratePublicationHtml --console=plain`
Expected: `BUILD SUCCESSFUL` and the file `build/dokka/html/index.html` exists.
Run: `ls build/dokka/html`
Expected: per-module output directories including `tree-structure`, `tree-structure-serialization`, `tree-structure-coroutines`, and `tree-structure-compose` (plus `index.html`, `navigation.html`, assets).
- [ ] **Step 4: Verify the four modules and root source links are present**
Run: `grep -roh "tree-structure-serialization\|tree-structure-coroutines\|tree-structure-compose" build/dokka/html/index.html | sort -u`
Expected: all three submodule names listed (confirming aggregation).
Run: `grep -rl "github.com/AdrianKuta/Tree-Data-Structure/blob/master/src/" build/dokka/html/tree-structure | head -1`
Expected: at least one file path printed (confirming the root/core module's source links resolve to `.../blob/master/src/...`). Optionally open `build/dokka/html/index.html` in a browser and click a core class's "source" link to confirm it lands on the right GitHub file.
- [ ] **Step 5: Commit**
```bash
git add build.gradle.kts
git commit -m "docs: aggregate all modules into one Dokka HTML site with source links (#32)"
```
---
## Task 3: Source links for the three submodules
The submodules are documented by the aggregation but need their own `sourceLink` so their symbols point at the correct subdirectory on GitHub. The block below is **path-derived and identical** for every module — add the exact same block to all three files.
**Files:**
- Modify: `tree-structure-serialization/build.gradle.kts`
- Modify: `tree-structure-coroutines/build.gradle.kts`
- Modify: `tree-structure-compose/build.gradle.kts`
- [ ] **Step 1: Add the `dokka {}` block to `tree-structure-serialization/build.gradle.kts`**
Add this top-level block (after the `repositories {}` block, before `kotlin {}`):
```kotlin
dokka {
dokkaSourceSets.configureEach {
sourceLink {
localDirectory.set(projectDir.resolve("src"))
val module = projectDir.relativeTo(rootDir).invariantSeparatorsPath
val prefix = if (module.isEmpty()) "" else "$module/"
remoteUrl("https://github.com/AdrianKuta/Tree-Data-Structure/blob/master/${prefix}src")
remoteLineSuffix.set("#L")
}
}
}
```
- [ ] **Step 2: Add the same block to `tree-structure-coroutines/build.gradle.kts`**
Add the identical block (after `repositories {}`, before `kotlin {}`):
```kotlin
dokka {
dokkaSourceSets.configureEach {
sourceLink {
localDirectory.set(projectDir.resolve("src"))
val module = projectDir.relativeTo(rootDir).invariantSeparatorsPath
val prefix = if (module.isEmpty()) "" else "$module/"
remoteUrl("https://github.com/AdrianKuta/Tree-Data-Structure/blob/master/${prefix}src")
remoteLineSuffix.set("#L")
}
}
}
```
- [ ] **Step 3: Add the same block to `tree-structure-compose/build.gradle.kts`**
Add the identical block (after the `repositories { mavenCentral(); google() }` block, before `kotlin {}`):
```kotlin
dokka {
dokkaSourceSets.configureEach {
sourceLink {
localDirectory.set(projectDir.resolve("src"))
val module = projectDir.relativeTo(rootDir).invariantSeparatorsPath
val prefix = if (module.isEmpty()) "" else "$module/"
remoteUrl("https://github.com/AdrianKuta/Tree-Data-Structure/blob/master/${prefix}src")
remoteLineSuffix.set("#L")
}
}
}
```
- [ ] **Step 4: Rebuild the site**
Run: `./gradlew :dokkaGeneratePublicationHtml --console=plain`
Expected: `BUILD SUCCESSFUL`.
- [ ] **Step 5: Verify each submodule's source links resolve to its subdirectory**
Run:
```bash
for m in serialization coroutines compose; do
echo -n "tree-structure-$m: "
grep -rl "github.com/AdrianKuta/Tree-Data-Structure/blob/master/tree-structure-$m/src/" build/dokka/html/tree-structure-$m | head -1 || echo "MISSING"
done
```
Expected: a file path printed for each of the three modules (none "MISSING").
- [ ] **Step 6: Commit**
```bash
git add tree-structure-serialization/build.gradle.kts tree-structure-coroutines/build.gradle.kts tree-structure-compose/build.gradle.kts
git commit -m "docs: add Dokka source links to serialization/coroutines/compose modules (#32)"
```
---
## Task 4: GitHub Pages workflow
**Files:**
- Create: `.github/workflows/docs.yml`
- [ ] **Step 1: Create the workflow file**
Create `.github/workflows/docs.yml` with exactly:
```yaml
name: Docs
on:
release:
types: [released]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: false
jobs:
build:
name: Build Dokka HTML
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
- name: Generate API docs
run: ./gradlew :dokkaGeneratePublicationHtml --console=plain
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v3
with:
path: build/dokka/html
deploy:
name: Deploy to GitHub Pages
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy
id: deployment
uses: actions/deploy-pages@v4
```
- [ ] **Step 2: Validate the YAML is well-formed**
Run: `ruby -ryaml -e "YAML.load_file('.github/workflows/docs.yml'); puts 'OK'"`
Expected: prints `OK` with no error. (Ruby ships with macOS. Alternatively, if `actionlint` is installed: `actionlint .github/workflows/docs.yml` → no output.)
- [ ] **Step 3: Commit**
```bash
git add .github/workflows/docs.yml
git commit -m "ci: add docs workflow to deploy Dokka HTML to GitHub Pages (#32)"
```
---
## Task 5: Link the site from the README
**Files:**
- Modify: `README.md` (top badge block, lines 1-6)
- [ ] **Step 1: Add the API docs badge**
In `README.md`, immediately after the existing `Publish` badge line:
```markdown
[![Publish](https://github.com/AdrianKuta/Tree-Data-Structure/actions/workflows/publishRelease.yml/badge.svg)](https://github.com/AdrianKuta/Tree-Data-Structure/actions/workflows/publishRelease.yml)
```
add this new line:
```markdown
[![API docs](https://img.shields.io/badge/docs-API%20reference-blue?style=plastic)](https://adriankuta.github.io/Tree-Data-Structure/)
```
- [ ] **Step 2: Add a one-line pointer under the badges**
After the badge block and its following blank line, and **before** the intro paragraph that starts `A lightweight n-ary tree for Kotlin Multiplatform.`, insert:
```markdown
📖 **[API reference](https://adriankuta.github.io/Tree-Data-Structure/)** — full KDoc for the core and all modules.
```
- [ ] **Step 3: Verify the link is present**
Run: `grep -n "adriankuta.github.io/Tree-Data-Structure" README.md`
Expected: two matches (the badge and the 📖 line).
- [ ] **Step 4: Commit**
```bash
git add README.md
git commit -m "docs: link the published API reference from the README (#32)"
```
---
## Task 6: Full verification, push, and pull request
**Files:** none (verification + integration)
- [ ] **Step 1: Full local verification**
Run each and confirm `BUILD SUCCESSFUL`:
```bash
./gradlew :dokkaGeneratePublicationHtml --console=plain # site builds, all 4 modules
./gradlew javadocJar --console=plain # release javadoc jar intact
./gradlew apiCheck --console=plain # binary-compat baseline unchanged
```
Expected: all three succeed. `apiCheck` must pass with no diff — this change touches only build config and docs, not public API. Optionally open `build/dokka/html/index.html` in a browser for a final visual check (module nav + a couple of source links).
- [ ] **Step 2: Push the branch**
```bash
git push -u origin docs/api-reference-github-pages
```
- [ ] **Step 3: Open the pull request**
```bash
gh pr create --base master --head docs/api-reference-github-pages \
--title "Publish API reference (Dokka HTML) to GitHub Pages (#32)" \
--body "$(cat <<'EOF'
Closes #32.
Migrates Dokka 1.9.20 → 2.2.0 (DGP v2) and publishes an aggregated, source-linked
multi-module API reference to GitHub Pages.
## What changed
- **Dokka 2.2.0 / DGP v2** (`V2Enabled` in `gradle.properties`). Gradle 8.5 and Kotlin
2.1.0 already satisfy Dokka 2.2.0's minimums (7.6+ / 1.9+), so no wrapper/Kotlin bump.
- **Aggregation**: the root (core) module pulls in `-serialization`, `-coroutines`, and
`-compose` via `dokka(project(...))` → one site at `build/dokka/html`
(`:dokkaGeneratePublicationHtml`).
- **Source links** on every module, pointing each symbol at its source on `master`.
- **`.github/workflows/docs.yml`**: builds the site and deploys via the official Pages
Actions on each release and on manual `workflow_dispatch`.
- **README**: docs badge + link to https://adriankuta.github.io/Tree-Data-Structure/
## Release pipeline unaffected
vanniktech 0.34.0 already supports Dokka `V2Enabled`, so the Maven Central
`-javadoc.jar` keeps building (verified locally with `./gradlew javadocJar`).
## ⚠️ One-time manual step required before the site goes live
Enable Pages: **Settings → Pages → Source = "GitHub Actions"**. Until then the
`deploy` job will fail. After enabling, run the **Docs** workflow once via
*Actions → Docs → Run workflow* (`workflow_dispatch`) to publish without waiting for a
release.
EOF
)"
```
Expected: PR URL printed.
- [ ] **Step 4: Post-merge manual steps (call these out to the repo owner)**
1. **Settings → Pages → Source = "GitHub Actions"** (one-time; otherwise `deploy` fails).
2. Trigger **Actions → Docs → Run workflow** (`workflow_dispatch`) to publish immediately.
3. Confirm the site is live at https://adriankuta.github.io/Tree-Data-Structure/ and the module nav + source links work.
---
## Self-Review
**Spec coverage:**
- Dokka 1.9.20 → 2.2.0 + V2 mode → Task 1 ✓
- Multi-module aggregation in root → Task 2 ✓
- Source links (root + 3 submodules) + site title → Tasks 2 & 3 ✓
- `docs.yml` (release + workflow_dispatch, official Pages Actions) → Task 4 ✓
- README badge + link → Task 5 ✓
- Verify javadoc jar / apiCheck / local docs build → Tasks 1, 2, 6 ✓
- Manual Pages-enable prerequisite → Tasks 4/6 PR body + post-merge steps ✓
- Out-of-scope items (versioned docs, Module.md, vanniktech swap, CI cache) → correctly excluded ✓
**Placeholder scan:** No TBD/TODO/"handle edge cases"/"similar to Task N". The identical source-link block is repeated in full in each of Task 3's steps (not referenced) ✓
**Type/name consistency:** Task name `:dokkaGeneratePublicationHtml`, output dir `build/dokka/html`, config functions (`moduleName.set`, `dokkaSourceSets.configureEach`, `sourceLink { localDirectory.set / remoteUrl / remoteLineSuffix.set }`), and `dokka(project(...))` aggregation are used identically across Tasks 16 and the workflow ✓
**Open risk carried from spec:** if Dokka cannot resolve Apple source sets on `ubuntu-latest` in CI (Task 4), switch the `build` job's `runs-on` to `macos-latest` (mirrors the iOS test job). Local verification runs on macOS, which covers Apple source sets.

View File

@@ -0,0 +1,237 @@
# Design: Publish API reference (Dokka HTML) to GitHub Pages
- **Issue:** [#32](https://github.com/AdrianKuta/Tree-Data-Structure/issues/32) — *Publish API reference (Dokka HTML) to GitHub Pages*
- **Date:** 2026-06-07
- **Status:** Approved (design); pending spec review
## Summary
Generate a browsable, multi-module API reference with Dokka and host it on GitHub
Pages. The site aggregates the core (`tree-structure`) plus the `-serialization`,
`-coroutines`, and `-compose` modules, links every symbol back to its source on
GitHub, and is (re)deployed on each GitHub release or on demand. The README points
to it.
## Background / current state
- **Dokka 1.9.20** is applied to all four modules (root core + three submodules) via
`alias(libs.plugins.dokka)`. Today it runs only because the **vanniktech
maven-publish plugin (0.34.0)** uses it to build the `-javadoc.jar` that Maven
Central requires. There is no aggregation config, no docs site, no source links.
- Plugin/library versions are centralized in `gradle/libs.versions.toml`.
- The **root project is itself the published core module** and the natural Dokka
aggregation root.
- Existing workflows: `test.yml` (reusable matrix: JVM/JS/Wasm/Native + `apiCheck`,
plus iOS on macOS) and `publishRelease.yml` (on GitHub release → tests →
`./gradlew publishToMavenCentral`). Both use `actions/checkout@v4` +
`actions/setup-java@v4` (temurin, JDK 21) with no Gradle cache action.
- Gradle wrapper **8.5**; Kotlin **2.1.0**.
### Verified compatibility (de-risking)
- **Dokka 2.2.0** (latest stable) requires **Gradle 7.6+** and **Kotlin 1.9+** → our
8.5 / 2.1.0 satisfy both. **No wrapper bump, no Kotlin bump.**
- **vanniktech 0.34.0** already supports Dokka `V2Enabled` (added in 0.30.0), so the
Maven Central `-javadoc.jar` keeps building under Dokka 2.x. **No vanniktech bump.**
(For reference, 0.36.0 *removes* Dokka v1 support entirely — so V2 is the forward
direction regardless.)
## Goals
1. Migrate Dokka `1.9.20``2.2.0` (DGP v2 / `V2Enabled`) without breaking the
release pipeline's javadoc jar.
2. Produce one aggregated multi-module HTML site for all four modules.
3. Link every documented symbol to its source on GitHub.
4. Deploy the site to GitHub Pages on each release and on manual dispatch.
5. Link the site from the README.
## Non-goals (per the issue's "follow-up" note)
- Versioned / per-release docs (one published version at a time, tracking the latest
release / manual run).
- Long-form `Module.md` package descriptions.
- Changing the publishing tool (vanniktech stays; out of scope for #32).
- Adding a Gradle cache action to CI (keep parity with existing workflows).
## Detailed design
### 1. Dokka 2.x migration
**`gradle/libs.versions.toml`**
```toml
dokka = "2.2.0" # was 1.9.20
```
**`gradle.properties`** — enable DGP v2 and silence the migration notice:
```properties
org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled
org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true
```
The four `alias(libs.plugins.dokka)` plugin applications stay unchanged.
### 2. Multi-module aggregation (root `build.gradle.kts`)
Add a top-level `dependencies { }` block declaring the three submodules as Dokka
aggregation inputs. The root documents itself and pulls in the three:
```kotlin
dependencies {
dokka(project(":tree-structure-serialization"))
dokka(project(":tree-structure-coroutines"))
dokka(project(":tree-structure-compose"))
}
```
- Generating task: **`:dokkaGeneratePublicationHtml`** (root project).
- Output directory: **`build/dokka/html`** (the aggregated site, default location).
### 3. Source links + site title
A `dokka { }` block is added to **each of the four module build files**. The
`sourceLink` derives the per-module GitHub path from the project layout so each
module is correct without hardcoding paths:
```kotlin
dokka {
dokkaSourceSets.configureEach {
sourceLink {
localDirectory.set(projectDir.resolve("src"))
val module = projectDir.relativeTo(rootDir).invariantSeparatorsPath
val prefix = if (module.isEmpty()) "" else "$module/"
remoteUrl("https://github.com/AdrianKuta/Tree-Data-Structure/blob/master/${prefix}src")
remoteLineSuffix.set("#L")
}
}
}
```
- For the **root** module, `module` is empty → links resolve to `…/blob/master/src/…`.
- For a submodule (e.g. serialization) → `…/blob/master/tree-structure-serialization/src/…`.
- Links point at the **`master`** branch. Trade-off: a previously deployed page links
to current `master`, which may have drifted. Accepted for simplicity; tag-accurate
links are a possible follow-up.
The **root** module additionally sets a friendly site title:
```kotlin
dokka {
moduleName.set("Tree Data Structure")
// ...sourceLink block as above...
}
```
Submodules keep their default module names (`tree-structure-serialization`,
`tree-structure-coroutines`, `tree-structure-compose`).
### 4. Pages workflow — `.github/workflows/docs.yml`
```yaml
name: Docs
on:
release:
types: [released]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: false
jobs:
build:
name: Build Dokka HTML
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
- name: Generate API docs
run: ./gradlew :dokkaGeneratePublicationHtml --console=plain
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v3
with:
path: build/dokka/html
deploy:
name: Deploy to GitHub Pages
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy
id: deployment
uses: actions/deploy-pages@v4
```
- Triggers: `release: [released]` (matches the issue) **+** `workflow_dispatch`
(manual rebuild without a release).
- Official GitHub Pages Actions (no `gh-pages` branch).
- Runs on `ubuntu-latest` to match `publishRelease.yml`. **Fallback:** if Dokka
cannot resolve the Apple source sets on Linux CI, switch the `build` job to
`macos-latest` (as the iOS test job already does).
### 5. README
In the badge row near the top, add a docs badge and a one-line pointer:
```markdown
[![API docs](https://img.shields.io/badge/docs-API%20reference-blue?style=plastic)](https://adriankuta.github.io/Tree-Data-Structure/)
```
Plus a short line under the badges:
```markdown
📖 **[API reference](https://adriankuta.github.io/Tree-Data-Structure/)** — full KDoc for all modules.
```
Site URL: `https://adriankuta.github.io/Tree-Data-Structure/`.
### 6. One-time manual step (repo owner, not code)
GitHub Pages must be enabled once: **Settings → Pages → Source = "GitHub Actions"**.
Until then the `deploy` job fails. This will be called out in the PR / final summary.
## Verification (before claiming done)
1. `./gradlew :dokkaGeneratePublicationHtml --console=plain` locally on **macOS**
(covers Apple source sets) → confirm `build/dokka/html/index.html` exists and the
site lists **all four** modules, with working source links.
2. `./gradlew javadocJar --console=plain` → confirm the vanniktech javadoc jar(s)
still build under Dokka 2.x (release pipeline unaffected). Optionally
`./gradlew publishToMavenLocal -Psnapshot=true` for an end-to-end check.
3. `./gradlew apiCheck` and the existing test tasks still pass (no API/source impact).
4. Validate `docs.yml` YAML (well-formed, correct action versions).
## Risks & mitigations
| Risk | Mitigation |
| --- | --- |
| Dokka 2.x breaks the javadoc jar | vanniktech 0.34.0 supports `V2Enabled`; verify with `javadocJar` step 2 above. |
| Dokka can't resolve Apple source sets on Linux CI | Verify locally on macOS; fallback `macos-latest` for the build job. |
| Source-link paths wrong for a module | Derived from `projectDir.relativeTo(rootDir)`; visually verify links in step 1. |
| Pages deploy fails on first run | Documented manual prerequisite (Settings → Pages → GitHub Actions). |
## Files touched
- `gradle/libs.versions.toml` — Dokka version bump.
- `gradle.properties``V2Enabled` flags.
- `build.gradle.kts` (root) — aggregation `dependencies`, `dokka { moduleName + sourceLink }`.
- `tree-structure-serialization/build.gradle.kts``dokka { sourceLink }`.
- `tree-structure-coroutines/build.gradle.kts``dokka { sourceLink }`.
- `tree-structure-compose/build.gradle.kts``dokka { sourceLink }`.
- `.github/workflows/docs.yml` — new Pages workflow.
- `README.md` — docs badge + link.

View File

@@ -1,2 +1,4 @@
kotlin.code.style=official
kotlin.js.compiler=ir
# Dokka Gradle Plugin v2 (https://kotl.in/dokka-gradle-migration)
org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled
org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true

24
gradle/libs.versions.toml Normal file
View File

@@ -0,0 +1,24 @@
[versions]
kotlin = "2.1.0"
dokka = "2.2.0"
mavenPublish = "0.34.0"
binaryCompatibilityValidator = "0.16.3"
kover = "0.8.3"
coroutines = "1.9.0"
kotlinxSerialization = "1.7.3"
composeMultiplatform = "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" }
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" }
composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
[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" }

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,5 @@
rootProject.name = "tree-structure"
include(":tree-structure-serialization")
include(":tree-structure-coroutines")
include(":tree-structure-compose")

View File

@@ -2,7 +2,7 @@ package com.github.adriankuta.datastructure.tree
import kotlin.jvm.JvmSynthetic
interface ChildDeclarationInterface<T> {
public interface ChildDeclarationInterface<T> {
/**
* This method is used to easily create child in node.
@@ -20,5 +20,5 @@ interface ChildDeclarationInterface<T> {
* @return New created TreeNode.
*/
@JvmSynthetic
fun child(value: T, childDeclaration: ChildDeclaration<T>? = null): TreeNode<T>
public fun child(value: T, childDeclaration: ChildDeclaration<T>? = null): TreeNode<T>
}

View File

@@ -9,16 +9,32 @@ 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<T>(val value: T, var treeIterator: TreeNodeIterators = PreOrder) : Iterable<TreeNode<T>>, ChildDeclarationInterface<T> {
public open class TreeNode<T>(public val value: T, public val treeIterator: TreeNodeIterators = PreOrder) : Iterable<TreeNode<T>>, ChildDeclarationInterface<T> {
private var _parent: TreeNode<T>? = null
/**
* The converse notion of a child, an immediate ancestor.
*/
val parent: TreeNode<T>?
public val parent: TreeNode<T>?
get() = _parent
private val _children = mutableListOf<TreeNode<T>>()
@@ -26,28 +42,60 @@ open class TreeNode<T>(val value: T, var treeIterator: TreeNodeIterators = PreOr
/**
* A group of nodes with the same parent.
*/
val children: List<TreeNode<T>>
public val children: List<TreeNode<T>>
get() = _children
/**
* Checks whether the current tree node is the root of the tree
* @return `true` if the current tree node is root of the tree, `false` otherwise.
*/
val isRoot: Boolean
public val isRoot: Boolean
get() = _parent == null
/**
* Add new child to current node or root.
* Adds [child] as a direct child of this node.
*
* @param child A node which will be directly connected to current node.
* @param child a node that is not already attached to a tree. To move a node that already has a
* parent, call [detach] on it first.
* @throws TreeNodeException if [child] already has a parent, or if attaching it here would create
* a cycle (i.e. [child] is this node or one of its ancestors).
*/
fun addChild(child: TreeNode<T>) {
public fun addChild(child: TreeNode<T>) {
if (child._parent != null) {
throw TreeNodeException("$child already has a parent; call detach() before re-attaching it.")
}
if (child === this) {
throw TreeNodeException("Adding $child here would create a cycle.")
}
// Only a node that already has its own subtree can contain `this` and thus form a cycle.
// Skipping this walk for leaves keeps building deep trees O(n) instead of O(n²).
if (child._children.isNotEmpty()) {
var ancestor: TreeNode<T>? = _parent
while (ancestor != null) {
if (ancestor === child) {
throw TreeNodeException("Adding $child here would create a cycle.")
}
ancestor = ancestor._parent
}
}
child._parent = this
_children.add(child)
}
/**
* Detaches this node from its parent, removing it from the parent's [children].
*
* @return `true` if this node was attached and is now detached; `false` if it was already a root.
*/
public fun detach(): Boolean {
val currentParent = _parent ?: return false
currentParent._children.remove(this)
_parent = null
return true
}
@JvmSynthetic
override fun child(value: T, childDeclaration: ChildDeclaration<T>?): TreeNode<T> {
public override fun child(value: T, childDeclaration: ChildDeclaration<T>?): TreeNode<T> {
val newChild = TreeNode(value)
newChild._parent = this
if (childDeclaration != null)
@@ -57,43 +105,55 @@ open class TreeNode<T>(val value: T, var treeIterator: TreeNodeIterators = PreOr
}
/**
* Removes a single instance of the specified node from this tree, if it is present.
* Removes [child] from this node's direct [children], if present.
*
* @return `true` if the node has been successfully removed; `false` if it was not present in the tree.
* This only removes a *direct* child of the receiver; it does not reach into other nodes. To
* remove a node from wherever it currently lives, call [detach] on it instead.
*
* @return `true` if [child] was a direct child and has been removed; `false` otherwise.
*/
fun removeChild(child: TreeNode<T>): Boolean {
println(child.value)
val removed = child._parent?._children?.remove(child)
child._parent = null
return removed ?: false
public fun removeChild(child: TreeNode<T>): Boolean {
val removed = _children.remove(child)
if (removed) child._parent = null
return removed
}
/**
* This function go through tree and counts children. Root element is not counted.
* @return All child and nested child count.
*/
fun nodeCount(): Int {
if (_children.isEmpty())
return 0
return _children.size +
_children.sumOf { it.nodeCount() }
public fun nodeCount(): Int {
var count = 0
val stack = ArrayDeque<TreeNode<T>>()
stack.addAll(_children)
while (stack.isNotEmpty()) {
val node = stack.removeLast()
count++
stack.addAll(node._children)
}
return count
}
/**
* @return The number of edges on the longest path between current node and a descendant leaf.
*/
fun height(): Int {
val childrenMaxDepth = _children.map { it.height() }
.maxOrNull()
?: -1 // -1 because this method counts nodes, and edges are always one less then nodes.
return childrenMaxDepth + 1
public fun height(): Int {
var maxDepth = 0
val stack = ArrayDeque<Pair<TreeNode<T>, Int>>()
stack.addLast(this to 0)
while (stack.isNotEmpty()) {
val (node, depthSoFar) = stack.removeLast()
if (depthSoFar > maxDepth) maxDepth = depthSoFar
node._children.forEach { stack.addLast(it to depthSoFar + 1) }
}
return maxDepth
}
/**
* Distance is the number of edges along the shortest path between two nodes.
* @return The distance between current node and the root.
*/
fun depth(): Int {
public fun depth(): Int {
var depth = 0
var tempParent = parent
@@ -105,44 +165,52 @@ open class TreeNode<T>(val value: T, var treeIterator: TreeNodeIterators = PreOr
}
/**
* Returns the collection of nodes, which connect the current node
* with its descendants
* Returns the chain of nodes from [descendant] up to and including this node, or `null` if
* [descendant] is not a strict descendant of this node.
*
* @param descendant the bottom child node for which the path is calculated
* @return collection of nodes, which connect the current node with its descendants
* @throws TreeNodeException exception that may be thrown in case if the
* current node does not have such descendant or if the
* specified tree node is root
* @param descendant the node to walk up from.
* @return the path `[descendant, …, this]`, or `null` if [descendant] is the root or is not
* located in this node's subtree.
*/
@Throws(TreeNodeException::class)
fun path(descendant: TreeNode<T>): List<TreeNode<T>> {
public fun path(descendant: TreeNode<T>): List<TreeNode<T>>? {
if (descendant.isRoot) return null
val path = mutableListOf<TreeNode<T>>()
var node = descendant
path.add(node)
while (!node.isRoot) {
node = node.parent!!
path.add(node)
if (node == this)
return path
if (node == this) return path
}
throw TreeNodeException("The specified tree node $descendant is not the descendant of tree node $this")
return null
}
/**
* Remove all children from root and every node in tree.
* Removes every descendant of this node. Afterwards [children] is empty and all former
* descendants are detached (their parent is `null`). This node itself stays attached to its own
* parent.
*/
fun clear() {
_parent = null
_children.forEach { it.clear() }
public fun clear() {
val descendants = ArrayDeque<TreeNode<T>>()
val stack = ArrayDeque<TreeNode<T>>()
stack.addAll(_children)
while (stack.isNotEmpty()) {
val node = stack.removeLast()
descendants.addLast(node)
stack.addAll(node._children)
}
descendants.forEach { node ->
node._parent = null
node._children.clear()
}
_children.clear()
}
override fun toString(): String {
public override fun toString(): String {
return value.toString()
}
fun prettyString(): String {
public fun prettyString(): String {
val stringBuilder = StringBuilder()
print(stringBuilder, "", "")
return stringBuilder.toString()
@@ -164,9 +232,14 @@ open class TreeNode<T>(val value: T, var treeIterator: TreeNodeIterators = PreOr
}
/**
* You can change default iterator by changing [treeIterator] property.
* Returns an iterator over this node and its descendants using the default [treeIterator] order.
* Use [iterator] with an explicit order, or the `asSequence(order)` extension, to traverse in a
* different order without changing this node's default.
*/
override fun iterator(): Iterator<TreeNode<T>> = when (treeIterator) {
public override fun iterator(): Iterator<TreeNode<T>> = iterator(treeIterator)
/** Returns an iterator over this node and its descendants in the given [order]. */
public fun iterator(order: TreeNodeIterators): Iterator<TreeNode<T>> = when (order) {
PreOrder -> PreOrderTreeIterator(this)
PostOrder -> PostOrderTreeIterator(this)
LevelOrder -> LevelOrderTreeIterator(this)

View File

@@ -3,7 +3,7 @@ package com.github.adriankuta.datastructure.tree
import com.github.adriankuta.datastructure.tree.iterators.TreeNodeIterators
import kotlin.jvm.JvmSynthetic
typealias ChildDeclaration<T> = ChildDeclarationInterface<T>.() -> Unit
public typealias ChildDeclaration<T> = ChildDeclarationInterface<T>.() -> Unit
/**
* This method can be used to initialize new tree.
@@ -14,7 +14,7 @@ typealias ChildDeclaration<T> = ChildDeclarationInterface<T>.() -> Unit
* @see [ChildDeclarationInterface.child]
*/
@JvmSynthetic
inline fun <reified T> tree(
public inline fun <reified T> tree(
root: T,
defaultIterator: TreeNodeIterators = TreeNodeIterators.PreOrder,
childDeclaration: ChildDeclaration<T>

View File

@@ -0,0 +1,65 @@
package com.github.adriankuta.datastructure.tree
/** Returns the first node (pre-order) whose value matches [predicate], or `null`. Short-circuits. */
public fun <T> TreeNode<T>.findNode(predicate: (T) -> Boolean): TreeNode<T>? =
preOrderSequence().firstOrNull { predicate(it.value) }
/** All nodes (pre-order) whose value matches [predicate]. */
public fun <T> TreeNode<T>.filterNodes(predicate: (T) -> Boolean): List<TreeNode<T>> =
preOrderSequence().filter { predicate(it.value) }.toList()
/** `true` if any node's value matches [predicate]. Short-circuits. */
public fun <T> TreeNode<T>.anyNode(predicate: (T) -> Boolean): Boolean =
preOrderSequence().any { predicate(it.value) }
/** `true` if every node's value matches [predicate]. Short-circuits. */
public fun <T> TreeNode<T>.allNodes(predicate: (T) -> Boolean): Boolean =
preOrderSequence().all { predicate(it.value) }
/** Counts nodes whose value matches [predicate]. */
public fun <T> TreeNode<T>.countNodes(predicate: (T) -> Boolean): Int =
preOrderSequence().count { predicate(it.value) }
/** Folds [operation] over all nodes in pre-order, starting from [initial]. */
public fun <T, R> TreeNode<T>.foldNodes(initial: R, operation: (acc: R, node: TreeNode<T>) -> R): R =
preOrderSequence().fold(initial) { acc, node -> operation(acc, node) }
/**
* Returns a new tree with the same shape whose values are produced by [transform]. The original is
* left untouched. Stack-safe (iterative), so it handles arbitrarily deep trees.
*/
public fun <T, R> TreeNode<T>.mapValues(transform: (T) -> R): TreeNode<R> {
val newRoot = TreeNode(transform(value), treeIterator)
val stack = ArrayDeque<Pair<TreeNode<T>, TreeNode<R>>>()
stack.addLast(this to newRoot)
while (stack.isNotEmpty()) {
val (source, target) = stack.removeLast()
source.children.forEach { child ->
val mappedChild = TreeNode(transform(child.value), child.treeIterator)
target.addChild(mappedChild)
stack.addLast(child to mappedChild)
}
}
return newRoot
}
/** Returns an independent deep copy of this subtree (same values, same shape, new nodes). */
public fun <T> TreeNode<T>.deepCopy(): TreeNode<T> = mapValues { it }
/**
* Structural equality: `true` when [other] holds the same values in the same shape. Unlike
* [TreeNode]'s reference equality, this compares the entire subtree. Stack-safe.
*/
public fun <T> TreeNode<T>.structurallyEquals(other: TreeNode<T>): Boolean {
val stack = ArrayDeque<Pair<TreeNode<T>, TreeNode<T>>>()
stack.addLast(this to other)
while (stack.isNotEmpty()) {
val (a, b) = stack.removeLast()
if (a.value != b.value) return false
if (a.children.size != b.children.size) return false
for (i in a.children.indices) {
stack.addLast(a.children[i] to b.children[i])
}
}
return true
}

View File

@@ -0,0 +1,41 @@
package com.github.adriankuta.datastructure.tree
/** `true` when this node has no children. */
public val <T> TreeNode<T>.isLeaf: Boolean get() = children.isEmpty()
/** The number of direct children of this node. */
public val <T> TreeNode<T>.degree: Int get() = children.size
/** Walks up the parent chain and returns the topmost ancestor (the tree root). */
public fun <T> TreeNode<T>.root(): TreeNode<T> {
var node = this
var parent = node.parent
while (parent != null) {
node = parent
parent = node.parent
}
return node
}
/** The chain of ancestors from the immediate [parent] up to (and including) the root. */
public fun <T> TreeNode<T>.ancestors(): List<TreeNode<T>> {
val result = mutableListOf<TreeNode<T>>()
var parent = this.parent
while (parent != null) {
result.add(parent)
parent = parent.parent
}
return result
}
/** The other children of this node's parent (excludes this node). Empty for the root. */
public fun <T> TreeNode<T>.siblings(): List<TreeNode<T>> =
parent?.children?.filter { it !== this } ?: emptyList()
/** All leaf nodes in this subtree, in pre-order. */
public fun <T> TreeNode<T>.leaves(): List<TreeNode<T>> =
preOrderSequence().filter { it.isLeaf }.toList()
/** All nodes in this subtree except this node, in pre-order. */
public fun <T> TreeNode<T>.descendants(): List<TreeNode<T>> =
preOrderSequence().filter { it !== this }.toList()

View File

@@ -0,0 +1,31 @@
package com.github.adriankuta.datastructure.tree
import com.github.adriankuta.datastructure.tree.iterators.LevelOrderTreeIterator
import com.github.adriankuta.datastructure.tree.iterators.PostOrderTreeIterator
import com.github.adriankuta.datastructure.tree.iterators.PreOrderTreeIterator
import com.github.adriankuta.datastructure.tree.iterators.TreeNodeIterators
/**
* Lazily traverses this subtree in the given [order] as a [Sequence], without forcing the whole
* traversal up front. Pairs with the Kotlin stdlib, e.g.
* `root.asSequence().map { it.value }.firstOrNull { it == target }`.
*/
public fun <T> TreeNode<T>.asSequence(
order: TreeNodeIterators = TreeNodeIterators.PreOrder,
): Sequence<TreeNode<T>> {
val self = this
return when (order) {
TreeNodeIterators.PreOrder -> Sequence { PreOrderTreeIterator(self) }
TreeNodeIterators.PostOrder -> Sequence { PostOrderTreeIterator(self) }
TreeNodeIterators.LevelOrder -> Sequence { LevelOrderTreeIterator(self) }
}
}
/** Lazy pre-order traversal as a [Sequence]. */
public fun <T> TreeNode<T>.preOrderSequence(): Sequence<TreeNode<T>> = asSequence(TreeNodeIterators.PreOrder)
/** Lazy post-order traversal as a [Sequence]. */
public fun <T> TreeNode<T>.postOrderSequence(): Sequence<TreeNode<T>> = asSequence(TreeNodeIterators.PostOrder)
/** Lazy level-order (breadth-first) traversal as a [Sequence]. */
public fun <T> TreeNode<T>.levelOrderSequence(): Sequence<TreeNode<T>> = asSequence(TreeNodeIterators.LevelOrder)

View File

@@ -2,5 +2,5 @@ package com.github.adriankuta.datastructure.tree.exceptions
import kotlin.jvm.JvmOverloads
class TreeNodeException @JvmOverloads constructor(message: String? = null, cause: Throwable? = null) :
public class TreeNodeException @JvmOverloads constructor(message: String? = null, cause: Throwable? = null) :
RuntimeException(message, cause)

View File

@@ -20,7 +20,7 @@ import com.github.adriankuta.datastructure.tree.TreeNode
* Output: 1 2 3 4 5 6 7 8 9 10 11 12 13
* ```
*/
class LevelOrderTreeIterator<T>(root: TreeNode<T>) : Iterator<TreeNode<T>> {
public class LevelOrderTreeIterator<T>(root: TreeNode<T>) : Iterator<TreeNode<T>> {
private val stack = ArrayDeque<TreeNode<T>>()
@@ -28,9 +28,9 @@ class LevelOrderTreeIterator<T>(root: TreeNode<T>) : Iterator<TreeNode<T>> {
stack.addLast(root)
}
override fun hasNext(): Boolean = stack.isNotEmpty()
public override fun hasNext(): Boolean = stack.isNotEmpty()
override fun next(): TreeNode<T> {
public override fun next(): TreeNode<T> {
val node = stack.removeFirst()
node.children
.forEach { stack.addLast(it) }

View File

@@ -20,29 +20,24 @@ import com.github.adriankuta.datastructure.tree.TreeNode
* Output: 10 5 11 12 13 6 2 3 7 8 9 4 1
* ```
*/
class PostOrderTreeIterator<T>(root: TreeNode<T>) : Iterator<TreeNode<T>> {
public class PostOrderTreeIterator<T>(root: TreeNode<T>) : Iterator<TreeNode<T>> {
private val stack = ArrayDeque<TreeNode<T>>()
private val result = ArrayDeque<TreeNode<T>>()
init {
stack.addAll(getChildrenStack(root))
}
override fun hasNext(): Boolean = stack.isNotEmpty()
override fun next(): TreeNode<T> {
return stack.removeFirst()
}
private fun getChildrenStack(node: TreeNode<T>): ArrayDeque<TreeNode<T>> {
// Iterative post-order: pop a node, prepend it to `result`, then push its children
// left-to-right. Reading `result` front-to-back yields post-order — without the deep
// recursion that overflowed the stack on degenerate (linear) trees.
val stack = ArrayDeque<TreeNode<T>>()
if(node.children.isEmpty()) {
return ArrayDeque(listOf(node))
stack.addLast(root)
while (stack.isNotEmpty()) {
val node = stack.removeLast()
result.addFirst(node)
node.children.forEach { stack.addLast(it) }
}
node.children.forEach {
stack.addAll(getChildrenStack(it))
}
stack.addLast(node)
return stack
}
public override fun hasNext(): Boolean = result.isNotEmpty()
public override fun next(): TreeNode<T> = result.removeFirst()
}

View File

@@ -20,7 +20,7 @@ import com.github.adriankuta.datastructure.tree.TreeNode
* Output: 1 2 5 10 6 11 12 13 3 4 7 8 9
* ```
*/
class PreOrderTreeIterator<T>(root: TreeNode<T>) : Iterator<TreeNode<T>> {
public class PreOrderTreeIterator<T>(root: TreeNode<T>) : Iterator<TreeNode<T>> {
private val stack = ArrayDeque<TreeNode<T>>()
@@ -28,9 +28,9 @@ class PreOrderTreeIterator<T>(root: TreeNode<T>) : Iterator<TreeNode<T>> {
stack.addLast(root)
}
override fun hasNext(): Boolean = stack.isNotEmpty()
public override fun hasNext(): Boolean = stack.isNotEmpty()
override fun next(): TreeNode<T> {
public override fun next(): TreeNode<T> {
val node = stack.removeLast()
node.children
.asReversed()

View File

@@ -5,7 +5,7 @@ package com.github.adriankuta.datastructure.tree.iterators
* @see PostOrder
* @see LevelOrder
*/
enum class TreeNodeIterators {
public enum class TreeNodeIterators {
/**
* Tree is iterated by using `Pre-order Traversal Algorithm"
* The pre-order traversal is a topologically sorted one,

View File

@@ -1,16 +0,0 @@
package com.github.adriankuta.datastructure.tree
import kotlin.test.Test
import kotlin.test.assertEquals
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

View File

@@ -0,0 +1,78 @@
package com.github.adriankuta.datastructure.tree
import kotlin.test.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotSame
import kotlin.test.assertNull
import kotlin.test.assertSame
import kotlin.test.assertTrue
class TreeNodeFunctionalTest {
private fun sample() = tree(1) {
child(2) {
child(4)
child(5)
}
child(3) {
child(6)
}
}
@Test
fun findNode() {
assertEquals(6, sample().findNode { it == 6 }?.value)
assertNull(sample().findNode { it == 99 })
}
@Test
fun filterNodes() =
assertContentEquals(listOf(2, 4, 6), sample().filterNodes { it % 2 == 0 }.map { it.value })
@Test
fun anyNode() {
assertTrue(sample().anyNode { it == 6 })
assertFalse(sample().anyNode { it == 99 })
}
@Test
fun allNodes() {
assertTrue(sample().allNodes { it > 0 })
assertFalse(sample().allNodes { it < 5 })
}
@Test
fun countNodes() = assertEquals(3, sample().countNodes { it > 3 })
@Test
fun foldNodes() = assertEquals(21, sample().foldNodes(0) { acc, node -> acc + node.value })
@Test
fun mapPreservesStructureAndTransformsValues() {
val mapped = sample().mapValues { it * 10 }
assertContentEquals(
listOf(10, 20, 40, 50, 30, 60),
mapped.preOrderSequence().map { it.value }.toList(),
)
}
@Test
fun deepCopyIsStructurallyEqualButDistinct() {
val original = sample()
val copy = original.deepCopy()
assertNotSame(original, copy)
assertTrue(original.structurallyEquals(copy))
}
@Test
fun structurallyEqualsDistinguishesByValueAndShape() {
assertTrue(sample().structurallyEquals(sample()))
val different = tree(1) {
child(2) { child(4) }
child(3) { child(6) }
}
assertFalse(sample().structurallyEquals(different))
}
}

View File

@@ -0,0 +1,66 @@
package com.github.adriankuta.datastructure.tree
import kotlin.test.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertSame
import kotlin.test.assertTrue
class TreeNodeNavigationTest {
private val root = TreeNode(1)
private val n2 = TreeNode(2)
private val n3 = TreeNode(3)
private val n4 = TreeNode(4)
private val n5 = TreeNode(5)
private val n6 = TreeNode(6)
init {
root.addChild(n2)
root.addChild(n3)
n2.addChild(n4)
n2.addChild(n5)
n3.addChild(n6)
}
@Test
fun isLeaf() {
assertTrue(n4.isLeaf)
assertFalse(root.isLeaf)
}
@Test
fun degree() {
assertEquals(2, root.degree)
assertEquals(0, n4.degree)
}
@Test
fun root() {
assertSame(root, n6.root())
assertSame(root, root.root())
}
@Test
fun ancestors() {
assertContentEquals(listOf(n2, root), n4.ancestors())
assertContentEquals(emptyList(), root.ancestors())
}
@Test
fun siblings() {
assertContentEquals(listOf(n5), n4.siblings())
assertContentEquals(emptyList(), root.siblings())
}
@Test
fun leaves() {
assertContentEquals(listOf(n4, n5, n6), root.leaves())
}
@Test
fun descendants() {
assertContentEquals(listOf(n2, n4, n5, n3, n6), root.descendants())
}
}

View File

@@ -0,0 +1,46 @@
package com.github.adriankuta.datastructure.tree
import com.github.adriankuta.datastructure.tree.iterators.TreeNodeIterators
import kotlin.test.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
class TreeNodeSequenceTest {
private fun sample() = tree(1) {
child(2) {
child(4)
child(5)
}
child(3) {
child(6)
}
}
@Test
fun preOrderSequence() =
assertContentEquals(listOf(1, 2, 4, 5, 3, 6), sample().preOrderSequence().map { it.value }.toList())
@Test
fun postOrderSequence() =
assertContentEquals(listOf(4, 5, 2, 6, 3, 1), sample().postOrderSequence().map { it.value }.toList())
@Test
fun levelOrderSequence() =
assertContentEquals(listOf(1, 2, 3, 4, 5, 6), sample().levelOrderSequence().map { it.value }.toList())
@Test
fun asSequenceDefaultsToPreOrder() =
assertContentEquals(listOf(1, 2, 4, 5, 3, 6), sample().asSequence().map { it.value }.toList())
@Test
fun asSequenceHonorsExplicitOrder() =
assertContentEquals(
listOf(1, 2, 3, 4, 5, 6),
sample().asSequence(TreeNodeIterators.LevelOrder).map { it.value }.toList(),
)
@Test
fun sequenceShortCircuitsLazily() =
assertEquals(4, sample().preOrderSequence().map { it.value }.first { it == 4 })
}

View File

@@ -0,0 +1,47 @@
package com.github.adriankuta.datastructure.tree
import com.github.adriankuta.datastructure.tree.iterators.TreeNodeIterators
import kotlin.test.Test
import kotlin.test.assertEquals
/**
* A deep, degenerate (linear) tree must not overflow the call stack. These tests build a chain
* thousands of nodes deep — recursive implementations of [TreeNode.height], [TreeNode.nodeCount]
* and the post-order iterator blow the stack here, so they pin the iterative rewrites.
*/
class TreeNodeStackSafetyTest {
private val depth = 50_000
private fun deepChain(): TreeNode<Int> {
val root = TreeNode(0)
var current = root
for (i in 1..depth) {
val child = TreeNode(i)
current.addChild(child)
current = child
}
return root
}
@Test
fun heightDoesNotOverflowOnDeepTree() {
assertEquals(depth, deepChain().height())
}
@Test
fun nodeCountDoesNotOverflowOnDeepTree() {
// nodeCount() excludes the root, so a chain of `depth` extra nodes counts as `depth`.
assertEquals(depth, deepChain().nodeCount())
}
@Test
fun postOrderIterationDoesNotOverflowOnDeepTree() {
assertEquals(depth + 1, deepChain().asSequence(TreeNodeIterators.PostOrder).count())
}
@Test
fun preOrderIterationDoesNotOverflowOnDeepTree() {
assertEquals(depth + 1, deepChain().asSequence(TreeNodeIterators.PreOrder).count())
}
}

View File

@@ -4,7 +4,9 @@ import com.github.adriankuta.datastructure.tree.iterators.TreeNodeIterators
import kotlin.test.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNull
import kotlin.test.assertTrue
class TreeNodeTest {
@@ -49,7 +51,7 @@ class TreeNodeTest {
)
root.removeChild(curdNode)
root.removeChild(gingerTeaNode)
gingerTeaNode.detach()
assertEquals(
"Root\n" +
"└── Beverages\n" +
@@ -62,6 +64,18 @@ class TreeNodeTest {
)
}
@Test
fun removeChildReturnsTrueWhenPresentFalseOtherwise() {
val root = TreeNode("Root")
val child = TreeNode("Child")
root.addChild(child)
assertTrue(root.removeChild(child), "Removing a present child returns true")
assertFalse(root.removeChild(child), "Removing an already-removed child returns false")
assertNull(child.parent, "Removed child is detached from its parent")
assertEquals(emptyList(), root.children)
}
@Test
fun clearTest() {
val root = TreeNode("Root")

View File

@@ -0,0 +1,53 @@
package com.github.adriankuta.datastructure.tree
import kotlin.test.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
import kotlin.test.assertNull
class TreeNodeUtilitiesTest {
private val root = TreeNode(1)
private val a = TreeNode(2)
private val b = TreeNode(3)
init {
root.addChild(a)
a.addChild(b)
}
@Test
fun nodeCountCountsDescendantsExcludingRoot() {
assertEquals(0, TreeNode("solo").nodeCount())
assertEquals(2, root.nodeCount())
}
@Test
fun heightIsLongestEdgePathToLeaf() {
assertEquals(0, TreeNode("solo").height())
assertEquals(2, root.height())
assertEquals(1, a.height())
}
@Test
fun depthIsDistanceToRoot() {
assertEquals(0, root.depth())
assertEquals(1, a.depth())
assertEquals(2, b.depth())
}
@Test
fun pathReturnsDescendantToReceiverChain() {
assertContentEquals(listOf(b, a, root), root.path(b))
}
@Test
fun pathReturnsNullWhenNotADescendant() {
assertNull(root.path(TreeNode(99)))
}
@Test
fun pathReturnsNullWhenDescendantIsRootItself() {
assertNull(root.path(root))
}
}

View File

@@ -0,0 +1,105 @@
package com.github.adriankuta.datastructure.tree
import com.github.adriankuta.datastructure.tree.exceptions.TreeNodeException
import com.github.adriankuta.datastructure.tree.iterators.TreeNodeIterators
import kotlin.test.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertFalse
import kotlin.test.assertNull
import kotlin.test.assertSame
import kotlin.test.assertTrue
class TreeNodeV4Test {
@Test
fun addChildRejectsNodeThatAlreadyHasAParent() {
val a = TreeNode("a")
val b = TreeNode("b")
a.addChild(b)
val other = TreeNode("other")
assertFailsWith<TreeNodeException> { other.addChild(b) }
}
@Test
fun addChildRejectsCycles() {
val root = TreeNode("root")
val child = TreeNode("child")
root.addChild(child)
// Attaching an ancestor under its own descendant would create a cycle.
assertFailsWith<TreeNodeException> { child.addChild(root) }
// Attaching a node under itself is also a cycle.
assertFailsWith<TreeNodeException> { root.addChild(root) }
}
@Test
fun detachRemovesFromParent() {
val root = TreeNode("root")
val child = TreeNode("child")
root.addChild(child)
assertTrue(child.detach())
assertNull(child.parent)
assertContentEquals(emptyList(), root.children)
// Detached node can now be re-attached elsewhere.
val newParent = TreeNode("newParent")
newParent.addChild(child)
assertSame(newParent, child.parent)
}
@Test
fun detachOnRootReturnsFalse() {
assertFalse(TreeNode("root").detach())
}
@Test
fun removeChildOnlyRemovesDirectChildren() {
val root = TreeNode("root")
val parent = TreeNode("parent")
val grandchild = TreeNode("grandchild")
root.addChild(parent)
parent.addChild(grandchild)
// grandchild is not a direct child of root -> no-op, returns false.
assertFalse(root.removeChild(grandchild))
assertSame(parent, grandchild.parent)
// direct child removal works.
assertTrue(parent.removeChild(grandchild))
assertNull(grandchild.parent)
}
@Test
fun clearOnNonRootKeepsItAttachedToItsParent() {
val root = TreeNode("root")
val branch = TreeNode("branch")
val leaf = TreeNode("leaf")
root.addChild(branch)
branch.addChild(leaf)
branch.clear()
assertContentEquals(emptyList(), branch.children)
assertSame(root, branch.parent) // branch stays attached to root
assertContentEquals(listOf(branch), root.children)
assertNull(leaf.parent) // former descendant is detached
}
@Test
fun iteratorAcceptsExplicitOrderWithoutMutatingDefault() {
val tree = tree(1) {
child(2) { child(4) }
child(3)
}
val postOrder = tree.iterator(TreeNodeIterators.PostOrder).asSequence().map { it.value }.toList()
assertContentEquals(listOf(4, 2, 3, 1), postOrder)
// Default order is unchanged (PreOrder).
assertEquals(TreeNodeIterators.PreOrder, tree.treeIterator)
assertContentEquals(listOf(1, 2, 4, 3), tree.map { it.value })
}
}

View File

@@ -1,14 +0,0 @@
import com.github.adriankuta.datastructure.tree.tree
val root =
tree("World") {
child("North America") {
child("USA")
}
child("Europe") {
child("Poland")
child("Germany")
}
}
print(root.prettyString())

View File

@@ -0,0 +1,4 @@
public final class com/github/adriankuta/datastructure/tree/compose/LazyTreeKt {
public static final fun LazyTree (Lcom/github/adriankuta/datastructure/tree/TreeNode;Landroidx/compose/ui/Modifier;ZLkotlin/jvm/functions/Function6;Landroidx/compose/runtime/Composer;II)V
}

View File

@@ -0,0 +1,87 @@
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler)
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-compose", version.toString())
pom {
name.set("Tree Data Structure — Compose Multiplatform")
description.set("A LazyTree composable (expand/collapse, lazy rendering) 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()
google()
}
dokka {
dokkaSourceSets.configureEach {
sourceLink {
// Resolve this module's GitHub source path relative to the repo root.
localDirectory.set(projectDir.resolve("src"))
val module = projectDir.relativeTo(rootDir).invariantSeparatorsPath
val prefix = if (module.isEmpty()) "" else "$module/"
remoteUrl("https://github.com/AdrianKuta/Tree-Data-Structure/blob/master/${prefix}src")
remoteLineSuffix.set("#L")
}
}
}
kotlin {
explicitApi()
jvmToolchain(21)
jvm()
@OptIn(ExperimentalWasmDsl::class)
wasmJs {
browser()
}
iosX64()
iosArm64()
iosSimulatorArm64()
sourceSets {
commonMain.dependencies {
api(project(":"))
implementation(compose.runtime)
implementation(compose.foundation)
}
}
}

View File

@@ -0,0 +1,71 @@
package com.github.adriankuta.datastructure.tree.compose
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import com.github.adriankuta.datastructure.tree.TreeNode
/**
* A lazily-rendered, expand/collapse tree for Compose Multiplatform. Only the currently-visible
* nodes are composed, so it scales to large trees. Expansion state is remembered internally, keyed
* by node identity.
*
* ```
* LazyTree(root) { node, depth, expanded, toggle ->
* Row(Modifier.padding(start = (depth * 16).dp).clickable(onClick = toggle)) {
* if (!node.isLeaf) Text(if (expanded) "▾" else "▸")
* Text(node.value.toString())
* }
* }
* ```
*
* @param root the root of the tree to display.
* @param modifier the [Modifier] applied to the underlying [LazyColumn].
* @param initiallyExpanded whether nodes start expanded.
* @param nodeContent renders a single node. Receives the node, its depth (root = 0), whether it is
* expanded, and a `toggle` callback that flips this node's expansion state.
*/
@Composable
public fun <T> LazyTree(
root: TreeNode<T>,
modifier: Modifier = Modifier,
initiallyExpanded: Boolean = true,
nodeContent: @Composable (node: TreeNode<T>, depth: Int, expanded: Boolean, toggle: () -> Unit) -> Unit,
) {
val expansion = remember(root) { mutableStateMapOf<TreeNode<T>, Boolean>() }
val isExpanded: (TreeNode<T>) -> Boolean = { node -> expansion[node] ?: initiallyExpanded }
val visible = flattenVisible(root, isExpanded)
LazyColumn(modifier = modifier) {
items(visible.size) { index ->
val (node, depth) = visible[index]
nodeContent(node, depth, isExpanded(node)) {
expansion[node] = !isExpanded(node)
}
}
}
}
/**
* Flattens the tree into the list of currently-visible `(node, depth)` pairs in pre-order, skipping
* the subtrees of collapsed nodes. Iterative, so it is safe on deep trees.
*/
private fun <T> flattenVisible(
root: TreeNode<T>,
isExpanded: (TreeNode<T>) -> Boolean,
): List<Pair<TreeNode<T>, Int>> {
val result = mutableListOf<Pair<TreeNode<T>, Int>>()
val stack = ArrayDeque<Pair<TreeNode<T>, Int>>()
stack.addLast(root to 0)
while (stack.isNotEmpty()) {
val (node, depth) = stack.removeLast()
result.add(node to depth)
if (isExpanded(node)) {
node.children.asReversed().forEach { child -> stack.addLast(child to depth + 1) }
}
}
return result
}

View File

@@ -0,0 +1,8 @@
public final class com/github/adriankuta/datastructure/tree/coroutines/TreeNodeFlowExtKt {
public static final fun asFlow (Lcom/github/adriankuta/datastructure/tree/TreeNode;Lcom/github/adriankuta/datastructure/tree/iterators/TreeNodeIterators;)Lkotlinx/coroutines/flow/Flow;
public static synthetic fun asFlow$default (Lcom/github/adriankuta/datastructure/tree/TreeNode;Lcom/github/adriankuta/datastructure/tree/iterators/TreeNodeIterators;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow;
public static final fun levelOrderFlow (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Lkotlinx/coroutines/flow/Flow;
public static final fun postOrderFlow (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Lkotlinx/coroutines/flow/Flow;
public static final fun preOrderFlow (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Lkotlinx/coroutines/flow/Flow;
}

View File

@@ -0,0 +1,102 @@
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()
}
dokka {
dokkaSourceSets.configureEach {
sourceLink {
// Resolve this module's GitHub source path relative to the repo root.
localDirectory.set(projectDir.resolve("src"))
val module = projectDir.relativeTo(rootDir).invariantSeparatorsPath
val prefix = if (module.isEmpty()) "" else "$module/"
remoteUrl("https://github.com/AdrianKuta/Tree-Data-Structure/blob/master/${prefix}src")
remoteLineSuffix.set("#L")
}
}
}
kotlin {
explicitApi()
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)
}
}
}

View File

@@ -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 <T> TreeNode<T>.asFlow(
order: TreeNodeIterators = TreeNodeIterators.PreOrder,
): Flow<TreeNode<T>> = asSequence(order).asFlow()
/** Pre-order traversal as a cold [Flow]. */
public fun <T> TreeNode<T>.preOrderFlow(): Flow<TreeNode<T>> = asFlow(TreeNodeIterators.PreOrder)
/** Post-order traversal as a cold [Flow]. */
public fun <T> TreeNode<T>.postOrderFlow(): Flow<TreeNode<T>> = asFlow(TreeNodeIterators.PostOrder)
/** Level-order (breadth-first) traversal as a cold [Flow]. */
public fun <T> TreeNode<T>.levelOrderFlow(): Flow<TreeNode<T>> = asFlow(TreeNodeIterators.LevelOrder)

View File

@@ -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(),
)
}
}

View File

@@ -0,0 +1,35 @@
public final class com/github/adriankuta/datastructure/tree/serialization/TreeNodeDto {
public static final field Companion Lcom/github/adriankuta/datastructure/tree/serialization/TreeNodeDto$Companion;
public fun <init> (Ljava/lang/Object;Ljava/util/List;)V
public synthetic fun <init> (Ljava/lang/Object;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Ljava/lang/Object;
public final fun component2 ()Ljava/util/List;
public final fun copy (Ljava/lang/Object;Ljava/util/List;)Lcom/github/adriankuta/datastructure/tree/serialization/TreeNodeDto;
public static synthetic fun copy$default (Lcom/github/adriankuta/datastructure/tree/serialization/TreeNodeDto;Ljava/lang/Object;Ljava/util/List;ILjava/lang/Object;)Lcom/github/adriankuta/datastructure/tree/serialization/TreeNodeDto;
public fun equals (Ljava/lang/Object;)Z
public final fun getChildren ()Ljava/util/List;
public final fun getValue ()Ljava/lang/Object;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}
public synthetic class com/github/adriankuta/datastructure/tree/serialization/TreeNodeDto$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public fun <init> (Lkotlinx/serialization/KSerializer;)V
public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/github/adriankuta/datastructure/tree/serialization/TreeNodeDto;
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/github/adriankuta/datastructure/tree/serialization/TreeNodeDto;)V
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
public final fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
}
public final class com/github/adriankuta/datastructure/tree/serialization/TreeNodeDto$Companion {
public final fun serializer (Lkotlinx/serialization/KSerializer;)Lkotlinx/serialization/KSerializer;
}
public final class com/github/adriankuta/datastructure/tree/serialization/TreeNodeDtoKt {
public static final fun toDto (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Lcom/github/adriankuta/datastructure/tree/serialization/TreeNodeDto;
public static final fun toTreeNode (Lcom/github/adriankuta/datastructure/tree/serialization/TreeNodeDto;)Lcom/github/adriankuta/datastructure/tree/TreeNode;
}

View File

@@ -0,0 +1,102 @@
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()
}
dokka {
dokkaSourceSets.configureEach {
sourceLink {
// Resolve this module's GitHub source path relative to the repo root.
localDirectory.set(projectDir.resolve("src"))
val module = projectDir.relativeTo(rootDir).invariantSeparatorsPath
val prefix = if (module.isEmpty()) "" else "$module/"
remoteUrl("https://github.com/AdrianKuta/Tree-Data-Structure/blob/master/${prefix}src")
remoteLineSuffix.set("#L")
}
}
}
kotlin {
explicitApi()
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"))
}
}
}

View File

@@ -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<TreeNodeDto<String>>(json).toTreeNode()
* ```
*/
@Serializable
public data class TreeNodeDto<T>(
public val value: T,
public val children: List<TreeNodeDto<T>> = emptyList(),
)
/** Converts this subtree into a serializable [TreeNodeDto], preserving values and shape. */
public fun <T> TreeNode<T>.toDto(): TreeNodeDto<T> =
TreeNodeDto(value, children.map { it.toDto() })
/** Rebuilds a mutable [TreeNode] tree from this DTO. */
public fun <T> TreeNodeDto<T>.toTreeNode(): TreeNode<T> {
val node = TreeNode(value)
children.forEach { node.addChild(it.toTreeNode()) }
return node
}

View File

@@ -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<TreeNodeDto<String>>(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)
}
}