Documentation as code

Posted by Frank Enders   on January 24, 2019 in Dev tagged with Development, Devops

Documentation as code

Technical documentation, if done at all, often tends to get outdated very quickly. One of the reasons for that often lies in having the documentation located somewhere far away from the subject it is documenting: the code.

The guys who are supposed to write the documentation face a media break when going to document what they were doing: They need to update a document which is not co-located to the code they were writing and perhaps even is not available in their IDE (e.g., a wiki or a word document somewhere in Sharepoint).

Even if that burden is taken and the documentation gets updated, it may contain duplicated content when it comes to code examples.

Having code examples in the documentation quickly violates the single source of truth. If the corresponding code changes, how am I sure that the documentation does not get outdated or even misleading at worst?

This Blog Post describes how we try to tackle this using Documentation as Code based on AsciiDoc.

AsciiDoc

AsciiDoc is a lightweight markup language, which is meant for writing technical documentation, articles or even presentations, books or prose. It started as an alternative to DocBook, which is XML based and thus brings much more markup overhead.

While being lightweight, it is expressive enough for writing the mentioned types of documents. That distinguishes AsciiDoc from other lightweight markup languages like e.g. Markdown (see e.g. Don’t use markdown for technical docs, AsciiDoc vs. Markdown).

Here are some of the things you can easily express in AsciiDoc:

Code blocks with syntax highlighting and callouts

There are source highlighters available for multiple languages. Here is an example for XML. The source also contains a so called callout, which is used to mark a specific position in the code, that can be referenced below the code block for further explanation:

[source,xml]
.~/.m2/settings.xml
----
<settings>
  <servers>
    <server>
      <id>path.to.repo</id>
      <username>USERNAME</username>
      <password>ENCRYPTED_PASSWORD</password><!--1-->
    </server>
  </servers>
</settings>
----
<1> In order to get your encrypted password, log in to Artifactory (http://path.to.repo). You can retrieve your encrypted password in your account's settings.

The result looks like this:

Rendered source block

Info-, warning- and further blocks

It is also very easy to emphasize different sections, e.g. for prominently placing a hint, a warning or an excursion:

TIP: Delimited block, showing a general tip.

IMPORTANT: Delimited block, showing an important information.

WARNING: Delimited block, showing a warning.

.Some excursion
****
Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim
****

This gets rendered to:

Rendered blocks

Tables

Tables can be easily created in various ways. One way is to use inline CSV format like this

[%header,format=csv]
|===
Author,Title,Year
Michael C. Feathers,Working Effectively with Legacy Code,2004
Robert C. Martin,Clean Code,2008
Joshua Bloch,Effective Java,2017
|===

which gets rendered to

Rendered table

AsciiDoc could also render that table directly from a CSV file from disk. That can be useful if you, for instance, want to include content of dynamically created CSV files (e.g. some kind of report done within the build) into your documentation.

|===
include::authors.csv[]
|===

Quotes

Adding nicely formatted quotes is also easy:

//variant one

[quote, Antoine de Saint-Exupery]
____
Perfection (in design) is achieved not when there is nothing more to add, but rather when there is nothing more to take away
____

//variant two

"Code never lies, comments sometimes do."
-- Ron Jeffries

This ends up in:

Rendered quotes

Diagrams

It can also render diagrams, which are written in Ascii using the asciidoctor-diagram extension.

So this:

                   +-------------+
                   | Asciidoctor |-------+
                   |   diagram   |       |
                   +-------------+       | PNG out
                       ^                 |
                       | ditaa in        |
                       |                 v
 +--------+   +--------+----+    /---------------\
 |        | --+ Asciidoctor +--> |               |
 |  Text  |   +-------------+    |   Beautiful   |
 |Document|   |   !magic!   |    |    Output     |
 |     {d}|   |             |    |               |
 +---+----+   +-------------+    \---------------/
     :                                   ^
     |          Lots of work             |
     +-----------------------------------+

gets converted to:

Rendered diagram

That allows for maintaining diagrams without the need for any additional UML tool. And, besides, you could use variables inside your diagrams, which get substituted during the build and thus are updated automatically.

Auto generated documentation

The best documentation might be the one you don’t need to write yourself and which gets updated automatically.

You can have both, when the documentation gets generated from sources which you maintain anyway. One example for that is when you have an API using the OpenAPI Specification (fka. Swagger).

Using Swagger2Markup, a human readable documentation can be generated right from your specification.

The famous Petshop would look like this:

Swagger Petshop

And lots more

The shown examples cover only a small set of the features offered by AsciiDoc. Besides, there are things like:

  • Table of Contents and Table of Figures
  • Bibliography
  • Footnotes

Multi-page documentation

Whereas you could create a one-page documentation with AsciiDoc, it is also suitable for creating large documents or even books. In the top level page, you specify the documentation’s meta data, like its title. Beside that, various aspects of the documentation can be configured.

= AsciiDoc Samples
// toc-title definition MUST follow document title without blank line!
:toc-title: Table of Contents
:toc: left
:toclevels: 3
:sectnums:
:sectnumlevels: 5
:imagesdir: ./images
:icons: font
:source-highlighter: coderay

Starting with the documentation’s title, a Table of Contents with three levels on the left side, having the title “Table of Contents” is configured. Below that, we configure sections to be numbered up to five levels. Defining the imagesdir allows us to include images across the documentation relative to that path. Setting the icons attribute to font tells AsciiDoc to use Font Awesome icons instead of Unicode glyphs, which normally looks better. And, finally, by specifying a source-highlighter, source highlighting for our code blocks gets enabled.

Following the configuration, we can start either adding content to the page or including other pages / chapters, using the include directive:

include::general.adoc[]

include::prerequisites.adoc[]

[...]

When dealing with larger documentation, you may want to add clickable links between different section of the documentation. You can place anchors in your documentation using [[UNIQUE_ANCHOR_NAME]]. Those can be referenced across the documentation via <<UNIQUE_ANCHOR_NAME>> or <<UNIQUE_ANCHOR_NAME,Title of the link>>.

Using AsciiDoctor to work with AsciiDoc sources

Sources written in AsciiDoc can be converted in multiple output formats. A widespread toolset for working with AsciiDoc is AsciiDoctor. It can be used standalone or as plugins for Maven or Gradle.

AsciiDoctor also provides good reference on how AsciiDoc can be used. The Quick Reference covers the most common cases. If you want to dive deeper, there is also a Full Guide available.

Keeping documentation together with your sources

Do you remember two of the difficulties of writing documentation? The documentation lives far away from what it is documenting and it tends to contain duplicated information, which outdates very quickly.

For both cases, AsciiDoc offers a solution. The documentation sources can reside right in the code repository. So the developers can just open and edit them directly within their IDE. In addition, some parts of the documentation, e.g. code samples, don’t need to be duplicated but can just included into your documentation.

In AsciiDoc you can include source files from the repository like this:

[source, java]
----
include: ../src/main/java/.../SomeClass.java[]
----

That class will be rendered inline within the documentation, having proper source highlighting.

Even if you don’t want to include a full source file but only one important part of it, AsciiDoc can help. For instance, we want to include the specific configuration of the maven-bundle-plugin, which adds the needed OSGi metada data to Java module.
In order not to include the whole pom in the documentation, we can mark the section of interest using a comment like <!-- tag::doc-service-impl-bundle-plugin-example[] --> and <!-- end::doc-service-impl-bundle-plugin-example[] --> in the sources.

A complete example looks like this:

...
<build>
    <plugins>
        <!-- tag::doc-service-impl-bundle-plugin-example[] -->
        <plugin>
            <groupId>org.apache.felix</groupId>
            <artifactId>maven-bundle-plugin</artifactId>
            <configuration>
                <instructions>
                    [...]
                </instructions>
            </configuration>
        </plugin>
        <!-- end::doc-service-impl-bundle-plugin-example[] -->
    </plugins>
</build>
...

The section between the comments can be included by specifying the corresponding tag:

[source,xml]
.pom.xml
----
include::../pom.xml[tags=doc-service-impl-bundle-plugin-example]
----

By that, the documentation gets updated automatically when you update your sources, without having any code duplication.

Release documentation together with code

A further benefit of having your documentation alongside you sources is that you can release them together. By that we get proper versioned documentation for free. Imagine you need to maintain an older version of your application and only have the latest documentation at hand.
Having the documentation within your repository allows to browse the documentation, which fits exactly to the code version you are dealing with.

We make use of this to also have proper versioned HTML and PDF conversions of our documentation to populate a documentation server with.
When doing a release of our application, the documentation gets archived in our documentation server.
Beside the latest version, we have a tagged version of our documentation available for each release of our application. Like you are used to for e.g. JavaDoc.

Archive containing all versions

Integrate AsciiDoctor in your build using Maven or Gradle

When having the documentation sources alongside your code, you would like to process those sources (e.g. converting them to HTML or PDF) the same way you process your code. For that, there are integrations for both, Maven and Gradle, available.

Ths post will concentrate on the integration via Maven using the asciidoctor-maven-plugin. The easiest integration, using default parameters, looks like this:

<plugins>
    <plugin>
        <groupId>org.asciidoctor</groupId>
        <artifactId>asciidoctor-maven-plugin</artifactId>
        <version>${asciidoctor.maven.plugin.version}</version> 
        ...
        <executions>
            <execution>
                <id>output-html</id>
                <phase>generate-resources</phase>
                <goals>
                    <goal>process-asciidoc</goal>
                </goals>
            </execution>
        </executions>
    </plugin>
</plugins>

By default, it will pick up resources in ${basedir}/src/main/asciidoc and converts them into HTML.

Normally, we use a distinct Maven profile for running AsciiDoc. By that we can easily decide whether to run AsciiDoc. You may want to skip it in order not to affect build time. Following example shows two executions, which convert the sources into HTML and PDF, using the corresponding backends. For converting into PDF, asciidoctorj-pdf is used, which is defined as a plugin dependency.

<profiles>
    <profile>
        <id>build-doc</id>
        <build>
            <defaultGoal>process-resources</defaultGoal>
            <plugins>
                <plugin>
                    <groupId>org.asciidoctor</groupId>
                    <artifactId>asciidoctor-maven-plugin</artifactId>
                    <version>${asciidoctor.maven.plugin.version}</version>
                    <configuration>
                        <sourceDirectory>src/main/doc</sourceDirectory>
                        <sourceDocumentName>documentation.adoc</sourceDocumentName>
                        <outputDirectory>${project.build.directory}/doc</outputDirectory>
                        <sourceHighlighter>rouge</sourceHighlighter>
                    </configuration>
                    <dependencies>
                        <dependency>
                            <groupId>org.asciidoctor</groupId>
                            <artifactId>asciidoctorj-pdf</artifactId>
                            <version>${asciidoctorj.pdf.version}</version>
                        </dependency>
                    </dependencies>
                    <executions>
                        <execution>
                            <id>generate-html</id>
                            <phase>generate-resources</phase>
                            <goals>
                                <goal>process-asciidoc</goal>
                            </goals>
                            <configuration>
                                <backend>html5</backend>
                            </configuration>
                        </execution>
                        <execution>
                            <id>generate-pdf</id>
                            <phase>generate-resources</phase>
                            <goals>
                                <goal>process-asciidoc</goal>
                            </goals>
                            <configuration>
                                <backend>pdf</backend>
                            </configuration>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </build>
    </profile>
</profiles>

Having that in place, you can build your documentation by invoking this profile:

mvn clean install -Pbuild-doc

Replacement for README.md

Having documentation near the code to make writing documentation easier is one thing. Another thing is to find documentation, when we need it. Since we have the documentation sources near to what it documents, it is easily to be found by developers. Due to its lean syntax even the sources are quite good to read for developers. Besides, there are plugins for all common IDEs, which render AsciiDoc. And even GitHub and GitLab render AsciiDoc directly in their graphical user interfaces. Both also recognize a README.adoc instead of a README.md and render its content as a repository’s description.

Here you can profit in two ways. First is you get a better expressiveness when using AsciiDoc (see e.g. the description of the asciidoctor-maven-plugin at Github). Second is you can “reuse” the README.adoc in the documentation you are going to convert to HTML or PDF by just including it as shown above.

Using AsciiDoctor locally

Setup tool chain

In order to manually convert .adoc files into HTML or PDF, without using maven or gradle, you can install the needed tools locally. The easiest way is to use RubyGems:

gem install asciidoctor

There are also alternative ways to install the tool chain.

Convert your first document

After having asciidoctor in place, you can convert any .adoc using.

asciidoctor documentation.adoc

That converts the document to HTML by default. You can specify a different output format by specifying a corresponding backend.

asciidoctor -b html5 documentation.adoc

Valid backends are html/html5, xhtml/xhtml5, docbook/docbook5, docbook45 or manpage.

There are multiple extensions for further output formats available. E.g. for PDF, EPUB3, LaTeX or Mallard.

Please refer to this page for a more detailed documentation.

Convert to PDF

To convert you documents into PDF, you first need to install the asciidoctor-pdf extension. Again, using RubyGems is the easiest:

gem install asciidoctor-pdf --pre

In order to get proper syntax highlighting for source code samples, you have in your files, you need to install a highlighter. Rouge currently is the recommended one.

gem install rouge

Afterwards, you can convert your document by just calling

asciidoctor-pdf documentation.adoc

Please refer to that document for a more detailed documentation.

Conclusion

No matter how good the tooling, having good documentation still requires continuous effort and discipline. Just like the code itself: when it does not get maintained continuously it will start to rot.

AsciiDoc offers a nice way to keep documentation together with your code and by that lowers the burden for developers to create documentation and to prevent ending up having lots of duplicated and outdated documentation.

AsciiDoc has much less markup overhead compared to DocBook. On the other hand is offers much more expressiveness as, for instance, Markdown. It is widely supported and is about to get an open specification within the Eclipse Foundation.