Skip to content

Commit

Permalink
refactor and improve documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
jillesvangurp committed Jan 12, 2024
1 parent 1d94f7c commit 1341560
Show file tree
Hide file tree
Showing 12 changed files with 299 additions and 77 deletions.
77 changes: 61 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,14 @@
[![](https://jitpack.io/v/jillesvangurp/kotlin4example.svg)](https://jitpack.io/#jillesvangurp/kotlin4example)
[![Actions Status](https://github.com/jillesvangurp/kotlin4example/workflows/CI-gradle-build/badge.svg)](https://github.com/jillesvangurp/kotlin4example/actions)

This project is an attempt at implementing [literate programming](https://en.wikipedia.org/wiki/Literate_programming) in Kotlin. Literate programming is useful
for documenting projects. You mix working code and documentation.
This project implements [literate programming](https://en.wikipedia.org/wiki/Literate_programming) in Kotlin. Literate programming is useful
for documenting projects. Having working code in your documentation, ensures that the examples you include are correct
and always up to date. And making it easy to include examples with your code lowers the barrier for writing good documentation.

The practice of copying code snippets to code blocks
inside markdown files is very brittle and leads to code that easily breaks. This project
solves this by generating the markdown from Kotlin with a simple DSL and provides
you with the tools to construct examples from working code.
## Get started


## Get it
Add the dependency to your project and start writing some documentation. See below for some examples.
I tend to put my documentation code in my tests so running the tests produces the documentation as a side effect.

```kotlin
implementation("com.github.jillesvangurp:kotlin4example:<version>")
Expand All @@ -27,16 +25,27 @@ repositories {
}
```

## Why another markdown snippet tool?
## Why another documentation tool?

When I started writing documentation for my [Kotlin Client for Elasticsearch](https://githubcom/jillesvangurp/es-kotlin-wrapper-client), I quickly discovered that keeping the
examples working was a big challenge.
examples in the documentation working was a challenge. I'd refactor or rename something which then would invalidate
all my examples. Staying on top of that is a lot of work.

Instead of just using one of the many documentation tools out there that can grab chunks of source code based on
some string marker, I instead came up with a better solution.

I wanted something that can leverage Kotlin's fantastic support for so-called internal DSLs. Like Ruby, you
can create domain specific languages using Kotlin's language features. In Kotlin, this works with regular functions
that take a block of code as a parameter. If such a parameter is the last one in a function, you can move the block outside
the parentheses. And if there are no other parameters those are optional. And then I realized that I could use
reflection to figure exactly from where the function call is made. This became the core
of what kotlin4example does. Any time you call example, it figures out from where in the code it is called and grabs the source
code in the block.

The library has a few other features, which are detailed in the examples below. But the simple idea is what
differentiates kotlin4example from other solutions. I'm not aware of any better or more convenient way to write
documentation for Kotlin libraries.

I fixed it by hacking together a solution to grab code samples from Kotlin through reflection and by making some assumptions about where source files are in a typical gradle project on github.

There are other tools that solve this problem. Usually this works by putting some strings in comments in your code and using some tool to dig out code snippets from the source code.

And there's of course nothing wrong with that approach and Kotlin4example actually also supports this. However, I wanted more. I wanted to actually run the snippets, be able to grab the output, and generate documentation using the Github flavor of markdown. Also, I did not want to deal with keeping track of snippet ids, their code comments, etc. Instead, I wanted to mix code and documentation and be able to refactor both code and documentation easily.

## Usage

Expand Down Expand Up @@ -183,8 +192,44 @@ class DocGenTest {
}
```

### Context receivers

A new feature in Kotlin that you currently have to opt into is context receivers.

Context receivers are useful for processing the output of your examples since you typically
need Kotlin4Example when you use the ExampleOutput.

I don't want
to force people to opt into context receivers yet but it's easy to add this yourself.

Simply add a simple extension function like this:.

```kotlin
context(Kotlin4Example)!
fun ExampleOutput<*>.printStdOut() {
+"""
This prints:
""".trimIndent()

mdCodeBlock(stdOut, type = "text", wrap = true)
}
```

And then you can use it `example { 1+1}.printStdOut()`.

To opt into context receivers, add this to your build file

```kotlin
kotlin {
compilerOptions {
freeCompilerArgs= listOf("-Xcontext-receivers")
}
}
```

For more elaborate examples of using this library, checkout my
[kt-search](https://github.com/jillesvangurp/kt-search) project. That
project is where this project emerged from and all markdown in that project is generated by kotlin4example.
project is where this project emerged from and all markdown in that project is generated by kotlin4example. Give it a
try on one of your own projects and let me know what you think.


28 changes: 19 additions & 9 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
@file:Suppress("GradlePackageVersionRange") // bs warning because we use refreshVersions

plugins {
kotlin("jvm")
`maven-publish`
id("org.jetbrains.dokka")
}

group = "com.jillesvangurp"
version = "1.0-SNAPSHOT"

repositories {
mavenCentral()
}
Expand Down Expand Up @@ -47,18 +47,28 @@ tasks.withType<Test> {
val artifactName = "kotlin4example"
val artifactGroup = "com.github.jillesvangurp"


val dokkaOutputDir = "${layout.buildDirectory.get()}/dokka"

tasks {
dokkaHtml {
outputDirectory.set(file(dokkaOutputDir))
}
}

val javadocJar by tasks.registering(Jar::class) {
dependsOn(tasks.dokkaHtml)
archiveClassifier.set("javadoc")
from(dokkaOutputDir)
}

publishing {
publications {
create<MavenPublication>("lib") {
groupId = artifactGroup
artifactId = artifactName
from(components["java"])
}
}
repositories {
maven {
name = "myRepo"
url = uri("file://${layout.buildDirectory.asFile.get().path}/repo")
artifact(javadocJar.get())
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.jillesvangurp.kotlin4example

import java.io.ByteArrayOutputStream
import java.io.PrintWriter

/**
* Simple facade that captures calls to print and println and collects
* what would have been printed in a buffer.
*/
class BlockOutputCapture {
private val byteArrayOutputStream = ByteArrayOutputStream()
private val printWriter = PrintWriter(byteArrayOutputStream)

fun print(message: Any?) {
printWriter.print(message)
}

fun println(message: Any?) {
printWriter.println(message)
}

fun output(): String {
printWriter.flush()
return byteArrayOutputStream.toString()
}

fun reset() {
printWriter.flush()
byteArrayOutputStream.reset()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.jillesvangurp.kotlin4example

/**
* When you use [Kotlin4Example.example] it uses this as the return value.
*/
data class ExampleOutput<T>(
val result: Result<T?>,
val stdOut: String,
)

0 comments on commit 1341560

Please sign in to comment.