Trivial Pursuit – API MarkDown

In diesen Beitrag geht es nicht um das gleichnamige Spiel, sondern um einen banalen Zeitvertreib eines Entwicklers.

Die Dokumentation einer REST API sieht immer sehr schick aus. Ob es sich um eine interaktive Swagger Seite handelt oder um eine statische Darstellung eines anderen Tools, alle Endpoints, Parameter und Datentypen sind akkurat aufgelistet.

Leider entspricht keine statische Darstellung den eigenen Anforderung nach einer einfachen MarkDown Datei für ein GitLab Projekt.

Um eine MarkDown Beschreibung zu generieren, wird eine maschinell verarbeitbare Darstellung der eigenen REST API benötigt und eine Transformation in das gewünschte Format.

Verwendet das eigene Projekt schon Swagger oder SpringDoc, so ist die erste Hürde schon genommen. Beide Tools liefern eine passende JSON Darstellung der REST API.

SpringDoc und Swagger können beide sehr einfach in das eigene Projekt integrieren werden. In diesem Beispiel wird SpringDoc verwendet und mit den folgenden Maven Dependencies erhält die eigene API unter /v3/api-docs einen Endpoint für die OpenApi Beschreibung.

<dependency>
  <groupId>org.springdoc</groupId>
  <artifactId>springdoc-openapi-ui</artifactId>
  <version>${springdoc-openapi.version}</version>
</dependency>
<dependency>
  <groupId>org.springdoc</groupId>
  <artifactId>springdoc-openapi-hateoas</artifactId>
  <version>${springdoc-openapi.version}</version>
</dependency>

Die OpenApi Beschreibung die der Endpoint liefert sieht wie folgt aus.

{
  "openapi": "3.0.1",
  "info": {
    "title": "Ancestors API",
    "version": "v1",
    "contact": {
      "email": "developer@schegge.de"
    }
  },
  "servers": [
    {
      "url": "http://localhost",
      "description": "Local test server"
    }
  ],
  "paths": { ... }
  "components": { ... }
}

Damit ist der erste Teil der eigenen Dokumentationsgenerierung schon geschafft. Nun muss die Beschreibung nur noch eingelesen werden und das MarkDown geschrieben werden.

Da das Eingangsformat JSON ist, ist das Importieren der Daten sehr einfach.

ObjectMapper objectMapper = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
ApiDescription description = objectMapper.readValue(reader, ApiDescription.class);

Mit dem Jackson ObjectMapper lesen wird den Inhalt der Datei in ein Object vom Typ ApiDescription ein. Die Klasse ApiDescription ist ein selbstgestriebenes POJO, das Attribute für alle Werte enthält, die dokumentiert werden sollen. Damit Jackson sich nicht über fehlende Attribute beschwert, wird dieses Feature vorher ausgeschaltet.

@Data
public class ApiDescription {
  private Info info;
  private List<Server> servers;
  private Map<String, Path> paths;
  private Components components;
}

Mit Lombok ist die Klasse schnell bereitgestellt. Darin enthalten sind weitere POJOS (Info, Server, Path, Components) für die Substrukturen der JSON Datei.

Als letzter Schritt der Generierung müssen die Daten in den POJOS im MarkDown Format in eine Datei geschrieben werden. Die Klasse MarkDownWriter fasst die allgemeinen Aufgaben der Generierung zusammen.

public class MarkDownWriter {
  ...

  public void write(ApiDescription description, String type, PrintWriter writer) throws IOException, TemplateException {
    Map<String, Schema> schemaMap = new SchemaCollector().visit(description, new HashMap<>());
    new PropertySchemaUpdater(schemaMap).visit(description, null);
    new EndPointUpdater(schemaMap).visit(description, null);

    cfg.getTemplate(type + ".ftl").process(Map.of("description", description), writer);
  }
}

Der MarkDownWriter verwendet FreeMarker um aus der übergebenen ApiDescription eine MarkDown Beschreibung zu erzeugen. In den ersten drei Zeilen der write Methode werden mit Hilfe von drei Visitor Implementierungen die Daten für die Ausgabe aufbereitet. Insbesondere werden Verweise auf die Schemata durch echte Referenzen auf die Instanzen ersetzt.

Danach wird die ApiDescription Instanz in den FreeMarker Kontext eingefügt und das gewählte Template prozessiert. Mit dem Parameter type wird ein spezielles MarkDown Template adressiert. Im Beispiel kann zwischen asciidoc und markdown gewählt werden.

Das entsprechende Template für AsciiDoc hat die folgende Form.

<#assign info = description.info>
<#assign servers = description.servers>
<#assign paths = description.paths>
<#assign schemas = description.components.schemas>
:icons: font
:fvicon:
:toc: macro
= ${info.title} <#if info.version??>(${info.version})</#if>
<#if (info.contact.email)??>

Contact: ${info.contact.email}
</#if>

${info.description!""}

toc::[]

<#if (servers)??>
== Servers
[%header,cols=2]
|===
| URL | Description
<#list servers as server>
|${server.url}
|${server.description}
</#list>
|===
</#if>
...

Die damit erzeugte AsciiDoc Datei für die Ancestor API liefert folgende Ausgabe.

Damit ist die Generierung einer MarkDown Beschreibung der eigenen API auch schon realisiert.