diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0b17b16..5b1f98e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,7 +23,7 @@ jobs: include: - name: JVM / JS / Wasm / Native + API check os: ubuntu-latest - tasks: jvmTest jsNodeTest wasmJsNodeTest nativeTest apiCheck + tasks: jvmTest jsNodeTest wasmJsNodeTest nativeTest apiCheck :samples:test :samples:run - name: iOS os: macos-latest tasks: iosSimulatorArm64Test diff --git a/README.md b/README.md index 1ec1c2a..a747d10 100644 --- a/README.md +++ b/README.md @@ -213,6 +213,15 @@ val bigger = root.addChild(ImmutableTreeNode("Asia")) // root is unchanged; bigg bigger.preOrder().forEach { println(it.value) } // pre/post/level-order, nodeCount(), height() ``` +## Examples + +A runnable `:samples` module bundles compile-checked, assertion-verified examples of the core API +and the serialization, coroutines, and immutable modules. Run them with: + +``` +./gradlew :samples:run +``` + ## Notes `TreeNode` is mutable and not thread-safe. Add your own synchronization if you share a tree across diff --git a/build.gradle.kts b/build.gradle.kts index 244b730..fcf6e83 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -59,6 +59,11 @@ repositories { mavenCentral() } +apiValidation { + // :samples is a dev-facing examples module, not a published artifact, so it has no .api dump. + ignoredProjects.add("samples") +} + dependencies { // Include this module's own docs in the aggregation — DGP v2 requires the // aggregating project to list itself explicitly. diff --git a/samples/build.gradle.kts b/samples/build.gradle.kts new file mode 100644 index 0000000..1d4c6cb --- /dev/null +++ b/samples/build.gradle.kts @@ -0,0 +1,35 @@ +plugins { + // No version: the Kotlin Gradle plugin is already on the build classpath via the root + // project's kotlinMultiplatform plugin, so requesting a version here would clash. + kotlin("jvm") + application +} + +kotlin { + jvmToolchain(21) +} + +application { + mainClass.set("com.github.adriankuta.samples.SamplesKt") +} + +repositories { + mavenCentral() +} + +dependencies { + implementation(project(":")) + implementation(project(":tree-structure-serialization")) + implementation(project(":tree-structure-coroutines")) + implementation(project(":tree-structure-immutable")) + // ImmutableTreeNode.children returns a PersistentList, so consumers that touch it need + // kotlinx.collections.immutable on their own classpath (the module declares it as implementation). + implementation(libs.kotlinx.collections.immutable) + + testImplementation(kotlin("test")) +} + +// kotlin("test") auto-selects the JUnit 5 adapter when the test task uses the JUnit Platform. +tasks.test { + useJUnitPlatform() +} diff --git a/samples/src/main/kotlin/com/github/adriankuta/samples/Samples.kt b/samples/src/main/kotlin/com/github/adriankuta/samples/Samples.kt new file mode 100644 index 0000000..b5368d6 --- /dev/null +++ b/samples/src/main/kotlin/com/github/adriankuta/samples/Samples.kt @@ -0,0 +1,121 @@ +package com.github.adriankuta.samples + +import com.github.adriankuta.datastructure.tree.TreeNode +import com.github.adriankuta.datastructure.tree.ancestors +import com.github.adriankuta.datastructure.tree.anyNode +import com.github.adriankuta.datastructure.tree.countNodes +import com.github.adriankuta.datastructure.tree.deepCopy +import com.github.adriankuta.datastructure.tree.distance +import com.github.adriankuta.datastructure.tree.filterNodes +import com.github.adriankuta.datastructure.tree.findNode +import com.github.adriankuta.datastructure.tree.isLeaf +import com.github.adriankuta.datastructure.tree.leaves +import com.github.adriankuta.datastructure.tree.levelOrderSequence +import com.github.adriankuta.datastructure.tree.lowestCommonAncestor +import com.github.adriankuta.datastructure.tree.mapValues +import com.github.adriankuta.datastructure.tree.pathBetween +import com.github.adriankuta.datastructure.tree.preOrderSequence +import com.github.adriankuta.datastructure.tree.structurallyEquals +import com.github.adriankuta.datastructure.tree.tree +import com.github.adriankuta.datastructure.tree.coroutines.asFlow +import com.github.adriankuta.datastructure.tree.coroutines.preOrderFlow +import com.github.adriankuta.datastructure.tree.immutable.ImmutableTreeNode +import com.github.adriankuta.datastructure.tree.immutable.preOrder +import com.github.adriankuta.datastructure.tree.iterators.TreeNodeIterators +import com.github.adriankuta.datastructure.tree.serialization.TreeNodeDto +import com.github.adriankuta.datastructure.tree.serialization.toDto +import com.github.adriankuta.datastructure.tree.serialization.toTreeNode +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.runBlocking +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +private fun sampleTree(): TreeNode = tree("World") { + child("North America") { + child("USA") + } + child("Europe") { + child("Poland") + child("Germany") + } +} + +/** Core API: DSL, pretty-print, traversal, navigation, functional, query, utilities, mutation. */ +fun coreSample(): String = buildString { + val root = sampleTree() + + appendLine("prettyString():") + append(root.prettyString()) + appendLine() + + appendLine("pre-order: " + root.preOrderSequence().map { it.value }.toList()) + appendLine("level-order: " + root.levelOrderSequence().map { it.value }.toList()) + + val usa = root.findNode { it == "USA" }!! + val poland = root.findNode { it == "Poland" }!! + appendLine("usa.depth(): " + usa.depth()) + appendLine("usa.ancestors(): " + usa.ancestors().map { it.value }) + appendLine("root.leaves(): " + root.leaves().map { it.value }) + appendLine("usa.isLeaf: " + usa.isLeaf) + + appendLine("anyNode == Poland: " + root.anyNode { it == "Poland" }) + appendLine("filterNodes len>5: " + root.filterNodes { it.length > 5 }.map { it.value }) + appendLine("countNodes 'U*': " + root.countNodes { it.startsWith("U") }) + appendLine("mapValues length: " + root.mapValues { it.length }.preOrderSequence().map { it.value }.toList()) + appendLine("deepCopy equals: " + root.structurallyEquals(root.deepCopy())) + + appendLine("lowestCommonAncestor(USA, Poland): " + usa.lowestCommonAncestor(poland)?.value) + appendLine("pathBetween(USA, Poland): " + usa.pathBetween(poland)?.map { it.value }) + appendLine("distance(USA, Poland): " + usa.distance(poland)) + + appendLine("nodeCount(): " + root.nodeCount()) + appendLine("height(): " + root.height()) + appendLine("path(USA): " + root.path(usa)?.map { it.value }) + + // Mutation on a copy; the shared sampleTree() stays untouched. + val mutable = root.deepCopy() + mutable.addChild(TreeNode("Asia")) + mutable.findNode { it == "Germany" }?.detach() + appendLine("after addChild(Asia) + detach(Germany): " + mutable.preOrderSequence().map { it.value }.toList()) +} + +/** Serialization satellite: TreeNode -> TreeNodeDto -> JSON -> TreeNodeDto -> TreeNode round-trip. */ +fun serializationSample(): String = buildString { + val root = sampleTree() + val json = Json.encodeToString(root.toDto()) + appendLine("JSON: $json") + val restored = Json.decodeFromString>(json).toTreeNode() + appendLine("round-trips structurallyEquals: " + root.structurallyEquals(restored)) +} + +/** Coroutines satellite: traverse the tree as a cold Flow. */ +fun coroutinesSample(): String = buildString { + val root = sampleTree() + val preOrder = runBlocking { root.preOrderFlow().map { it.value }.toList() } + val levelOrder = runBlocking { root.asFlow(TreeNodeIterators.LevelOrder).map { it.value }.toList() } + appendLine("preOrderFlow(): $preOrder") + appendLine("asFlow(LevelOrder): $levelOrder") +} + +/** Immutable satellite: persistent tree; every op returns a new root, leaving the original intact. */ +fun immutableSample(): String = buildString { + val root = ImmutableTreeNode("World").addChild(ImmutableTreeNode("Europe")) + val bigger = root.addChild(ImmutableTreeNode("Asia")) + appendLine("root.children: " + root.children.map { it.value }) + appendLine("bigger.children: " + bigger.children.map { it.value }) + appendLine("root unchanged: " + (root.children.size == 1)) + appendLine("bigger.mapValues uppercase preOrder: " + bigger.mapValues { it.uppercase() }.preOrder().map { it.value }) +} + +fun main() { + println("== Core ==") + println(coreSample()) + println("== Serialization ==") + println(serializationSample()) + println("== Coroutines ==") + println(coroutinesSample()) + println("== Immutable ==") + println(immutableSample()) +} diff --git a/samples/src/test/kotlin/com/github/adriankuta/samples/SamplesTest.kt b/samples/src/test/kotlin/com/github/adriankuta/samples/SamplesTest.kt new file mode 100644 index 0000000..3068cdc --- /dev/null +++ b/samples/src/test/kotlin/com/github/adriankuta/samples/SamplesTest.kt @@ -0,0 +1,46 @@ +package com.github.adriankuta.samples + +import kotlin.test.Test +import kotlin.test.assertContains + +class SamplesTest { + + @Test + fun coreSampleRendersTreeAndTraversals() { + val out = coreSample() + assertContains( + out, + "World\n" + + "├── North America\n" + + "│ └── USA\n" + + "└── Europe\n" + + " ├── Poland\n" + + " └── Germany\n", + ) + assertContains(out, "[World, North America, USA, Europe, Poland, Germany]") + assertContains(out, "[North America, World]") // usa.ancestors() + assertContains(out, "[USA, Poland, Germany]") // root.leaves() + } + + @Test + fun serializationSampleRoundTrips() { + val out = serializationSample() + assertContains(out, "\"World\"") + assertContains(out, "round-trips structurallyEquals: true") + } + + @Test + fun coroutinesSampleCollectsFlows() { + val out = coroutinesSample() + assertContains(out, "preOrderFlow(): [World, North America, USA, Europe, Poland, Germany]") + assertContains(out, "asFlow(LevelOrder): [World, North America, Europe, USA, Poland, Germany]") + } + + @Test + fun immutableSampleLeavesRootUnchanged() { + val out = immutableSample() + assertContains(out, "root.children: [Europe]") + assertContains(out, "bigger.children: [Europe, Asia]") + assertContains(out, "root unchanged: true") + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 2daffa2..dee42cf 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -4,3 +4,4 @@ include(":tree-structure-serialization") include(":tree-structure-coroutines") include(":tree-structure-compose") include(":tree-structure-immutable") +include(":samples")