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.
This commit is contained in:
2026-06-07 19:50:02 +02:00
parent bec1fe02a7
commit a7018b8c61
2 changed files with 106 additions and 141 deletions

View File

@@ -6,6 +6,11 @@ All notable changes to this project are documented here. The format is based on
## [Unreleased] ## [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] ## [4.0.0]
A breaking release that cleans up the core API and enforces an explicit public surface. A breaking release that cleans up the core API and enforces an explicit public surface.

242
README.md
View File

@@ -3,16 +3,23 @@
[![License: MIT](https://img.shields.io/github/license/AdrianKuta/Tree-Data-Structure?style=plastic)](https://github.com/AdrianKuta/Tree-Data-Structure/blob/master/LICENSE) [![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) [![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)
Lightweight Kotlin Multiplatform tree data structure for Kotlin and Java. Includes a small DSL, multiple traversal iterators, and pretty-print support. 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.
- Kotlin Multiplatform (JVM, JS, Wasm, iOS, and Native host) It fits homogeneous trees of arbitrary depth: UI component hierarchies, file-system views, org
- Pre-order, Post-order, and Level-order iteration charts, and category menus. For fixed, typed hierarchies (like a compiler AST) a sealed class is
- Lazy `Sequence` traversal that composes with the Kotlin stdlib (`map`/`filter`/`firstOrNull`…) usually a better fit.
- Navigation helpers: `root()`, `ancestors()`, `siblings()`, `leaves()`, `descendants()`, `isLeaf`, `degree`
- Functional helpers: `findNode`, `filterNodes`, `anyNode`, `allNodes`, `foldNodes`, `mapValues`, `deepCopy`, `structurallyEquals` ## Features
- Stack-safe: traversal and `height()`/`nodeCount()`/`clear()` handle arbitrarily deep trees without `StackOverflowError`
- Simple DSL: tree { child(...) } - Kotlin Multiplatform: JVM, JS, Wasm, iOS, and a native host target.
- Utilities: nodeCount(), height(), depth(), path(), prettyString(), clear(), removeChild() - 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 ## Installation
@@ -20,14 +27,14 @@ Gradle (Kotlin DSL):
```kotlin ```kotlin
// commonMain for KMP projects, or any sourceSet/module where you need it // commonMain for KMP projects, or any sourceSet/module where you need it
dependencies { dependencies {
implementation("com.github.adriankuta:tree-structure:4.0.0") // 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): Gradle (Groovy):
```groovy ```groovy
dependencies { dependencies {
implementation "com.github.adriankuta:tree-structure:4.0.0" // see badge above for the latest implementation "com.github.adriankuta:tree-structure:4.0.0"
} }
``` ```
@@ -40,27 +47,9 @@ Maven:
</dependency> </dependency>
``` ```
## Usage ## Building a tree
**Kotlin** The DSL is the shortest way to build one:
```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)**
```kotlin ```kotlin
val root = tree("World") { val root = tree("World") {
child("North America") { child("USA") } child("North America") { child("USA") }
@@ -71,136 +60,123 @@ val root = tree("World") {
} }
``` ```
**Java** The same node-by-node API works from Kotlin and Java:
```java ```java
TreeNode<String> root = new TreeNode<>("World"); 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"); TreeNode<String> europe = new TreeNode<>("Europe");
root.addChild(northA);
root.addChild(europe); root.addChild(europe);
europe.addChild(new TreeNode<>("Poland"));
TreeNode<String> usa = new TreeNode<>("USA"); europe.addChild(new TreeNode<>("Germany"));
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());
``` ```
Output: `prettyString()` renders the tree for logs and debugging:
``` ```
World World
├── North America ├── North America
│ └── USA │ └── USA
└── Europe └── Europe
├── Poland ├── Poland
└── France └── Germany
``` ```
### Traversal and utilities ## Traversal
```kotlin
val root = TreeNode("root")
// ... build your tree
// Choose iteration order per call (the default order is set in the constructor and is read-only) 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
for (node in root) println(node.value) // default pre-order
for (node in root.asSequence(TreeNodeIterators.PostOrder)) println(node.value) for (node in root.asSequence(TreeNodeIterators.PostOrder)) 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 — removeChild removes a *direct* child; detach() unhooks a node from wherever it lives
val child = root.children.first()
root.removeChild(child) // child is now detached from root
root.clear() // remove all descendants of root
``` ```
### Lazy traversal with Sequence Traversal is also exposed as a lazy `Sequence`, so it composes with the standard library and stops
early instead of materializing the whole tree:
Traversal is exposed as a lazy `Sequence`, so it composes with the Kotlin standard library and
short-circuits (it never materializes the whole tree just to find one node):
```kotlin ```kotlin
val tree = tree("World") { root.preOrderSequence().map { it.value }.toList() // [World, North America, USA, Europe, Poland, Germany]
child("North America") { child("USA") } root.levelOrderSequence().first { it.value == "USA" } // stops as soon as it is found
child("Europe") { root.asSequence(TreeNodeIterators.PostOrder).count() // 6
child("Poland")
child("Germany")
}
}
// Pick an order explicitly — no need to mutate the node's state.
tree.preOrderSequence().map { it.value }.toList() // [World, North America, USA, Europe, Poland, Germany]
tree.levelOrderSequence().first { it.value == "USA" } // stops as soon as it's found
tree.asSequence(TreeNodeIterators.PostOrder).count() // 6
``` ```
### Navigation ## Navigation
```kotlin ```kotlin
val usa = tree.findNode { it == "USA" }!! val usa = root.findNode { it == "USA" }!!
usa.isLeaf // true usa.isLeaf // true
usa.depth() // 2 usa.depth() // 2
usa.root().value // "World" usa.root().value // "World"
usa.ancestors().map { it.value } // [North America, World] usa.ancestors().map { it.value } // [North America, World]
tree.leaves().map { it.value } // [USA, Poland, Germany] root.leaves().map { it.value } // [USA, Poland, Germany]
val europe = tree.findNode { it == "Europe" }!!
europe.children.first().siblings().map { it.value } // [Germany]
``` ```
### Functional operations ## Functional operations
```kotlin ```kotlin
tree.anyNode { it == "Poland" } // true root.anyNode { it == "Poland" } // true
tree.filterNodes { it.length > 5 } // nodes whose value is longer than 5 chars root.filterNodes { it.length > 5 } // nodes whose value is longer than 5 characters
tree.countNodes { it.startsWith("U") } // 1 root.countNodes { it.startsWith("U") } // 1
// Transform values into a brand-new tree (the original is untouched); stack-safe. val lengths: TreeNode<Int> = root.mapValues { it.length } // a new tree; the original is untouched
val lengths: TreeNode<Int> = tree.mapValues { it.length } val copy = root.deepCopy()
root.structurallyEquals(copy) // true: same values and shape, different nodes
```
// Deep copy + structural comparison. ## Utilities
val copy = tree.deepCopy() ```kotlin
tree.structurallyEquals(copy) // true (same values, same shape, different nodes) 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 ## Optional modules
The core `tree-structure` artifact has no third-party dependencies. Ecosystem integrations ship as The core artifact has no third-party dependencies. Each integration is a separate, opt-in artifact
separate, opt-in artifacts that depend on the core. that depends on the core.
### Serialization `tree-structure-serialization` ### Serialization (`tree-structure-serialization`)
`kotlinx.serialization` support. A `TreeNode` holds a parent back-reference (a cycle), so it cannot `kotlinx.serialization` support. A `TreeNode` keeps a reference back to its parent, so it cannot be
be `@Serializable` directly — convert to/from the acyclic `TreeNodeDto` instead. `@Serializable` directly. Convert to and from the acyclic `TreeNodeDto` instead.
```kotlin ```kotlin
implementation("com.github.adriankuta:tree-structure-serialization:4.0.0") implementation("com.github.adriankuta:tree-structure-serialization:4.0.0")
``` ```
```kotlin ```kotlin
val json = Json.encodeToString(tree.toDto()) val json = Json.encodeToString(root.toDto())
val restored = Json.decodeFromString<TreeNodeDto<String>>(json).toTreeNode() val restored = Json.decodeFromString<TreeNodeDto<String>>(json).toTreeNode()
``` ```
### Coroutines `tree-structure-coroutines` ### Coroutines (`tree-structure-coroutines`)
Traverse a tree as a cold `Flow` (handy in coroutine/`ViewModel` pipelines). Traverse a tree as a cold `Flow`, which is handy inside coroutine and `ViewModel` pipelines.
```kotlin ```kotlin
implementation("com.github.adriankuta:tree-structure-coroutines:4.0.0") implementation("com.github.adriankuta:tree-structure-coroutines:4.0.0")
``` ```
```kotlin ```kotlin
tree.preOrderFlow().collect { println(it.value) } root.preOrderFlow().collect { println(it.value) }
tree.asFlow(TreeNodeIterators.LevelOrder).map { it.value } root.asFlow(TreeNodeIterators.LevelOrder).map { it.value }
``` ```
### Compose UI `tree-structure-compose` ### Compose UI (`tree-structure-compose`)
A `LazyTree` composable for Compose Multiplatform (JVM/desktop, iOS, Wasm). Only the visible nodes A `LazyTree` composable for Compose Multiplatform (JVM/desktop, iOS, Wasm). Only the visible nodes
are composed, and you decide how each node looks: are composed, and you decide how each node looks:
@@ -208,7 +184,6 @@ are composed, and you decide how each node looks:
```kotlin ```kotlin
implementation("com.github.adriankuta:tree-structure-compose:4.0.0") implementation("com.github.adriankuta:tree-structure-compose:4.0.0")
``` ```
```kotlin ```kotlin
LazyTree(root) { node, depth, expanded, toggle -> LazyTree(root) { node, depth, expanded, toggle ->
Row(Modifier.padding(start = (depth * 16).dp).clickable(onClick = toggle)) { Row(Modifier.padding(start = (depth * 16).dp).clickable(onClick = toggle)) {
@@ -218,37 +193,24 @@ LazyTree(root) { node, depth, expanded, toggle ->
} }
``` ```
## Publishing to Maven Central (central.sonatype.com) ## Notes
This project is configured to publish artifacts to Maven Central via the Sonatype Central Portal. `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.
There are two supported ways to publish: Coming from 3.x? See [CHANGELOG.md](CHANGELOG.md) for the 4.0 migration notes.
1) Via GitHub Actions (recommended) ## Releasing (maintainers)
- 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.
2) Locally via Gradle Releases go to Maven Central through the Sonatype Central Portal using the
- Ensure you have a Sonatype Central account and that the groupId com.github.adriankuta is verified in central.sonatype.com (Namespace Rules → Verify). `com.vanniktech.maven.publish` plugin. Creating a GitHub release runs
- Export the same credentials/signing values as environment variables or pass them as Gradle properties: `.github/workflows/publishRelease.yml`, which signs and uploads every module; the deployment is then
- ORG_GRADLE_PROJECT_mavenCentralUsername published from central.sonatype.com. The published version comes from `PUBLISH_VERSION` in
- ORG_GRADLE_PROJECT_mavenCentralPassword `build.gradle.kts`. CI needs the `MAVEN_CENTRAL_USERNAME`, `MAVEN_CENTRAL_PASSWORD`, `SIGNING_KEY`,
- ORG_GRADLE_PROJECT_signingInMemoryKey and `SIGNING_PASSWORD` repository secrets. To publish from a local machine, set the matching
- ORG_GRADLE_PROJECT_signingInMemoryKeyPassword `ORG_GRADLE_PROJECT_*` properties and run `./gradlew publishToMavenCentral` (add `-Psnapshot=true`
- Then run: for a snapshot build).
- ./gradlew publishToMavenCentral
- For snapshot publishing, set -Psnapshot=true (the version is derived from PUBLISH_VERSION with -SNAPSHOT).
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.
## License ## License
@@ -273,5 +235,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, 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 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.
---