Make documentation great again (Bonus)

15 novembre 2021

Cet article fait partie de la série "Make documentation great again"

Après la présentation générale de Spring REST Docs, je vous propose d’aller un peu plus loin en nous penchant sur des petites améliorations qui feront toute la différence !

Nous continuerons de nous baser sur mon projet de démonstration disponible sur GitHub.

Exposer "automagiquement" la documentation

Nous l’avons vu : nous sommes désormais capables de générer une page HTML contenant la documentation de notre API. Mais ce n’est pas le meilleur moyen de la rendre disponible aux consommateurs de l’API, nous pouvons faire mieux que ça !

Et quel meilleur emplacement que l’application Spring Boot en elle-même ? Pour cela, il faut un peu de paramétrage, grâce au plugin Maven maven-resources-plugin.

            <plugin>
                <artifactId>maven-resources-plugin</artifactId>
                <executions>
                    <execution>
                        <id>copy-resources</id>
                        <phase>prepare-package</phase>
                        <goals>
                            <goal>copy-resources</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>${project.build.outputDirectory}/static/docs/</outputDirectory>
                            <resources>
                                <resource>
                                    <directory>${project.build.directory}/generated-docs</directory>
                                </resource>
                            </resources>
                        </configuration>
                    </execution>
                </executions>
            </plugin>

Ce code permet de copier le contenu du répertoire target/generated-docs dans un répertoire target/static-docs/docs. Autrement dit la page HTML générée dans un dossier un peu particulier, puisque static est automatiquement servi par Spring. Si vous voulez en savoir plus sur cette caractéristique, Baeldung a évidemment une page dédiée !

Avec cette configuration, si vous lancez la construction de l’application (mvn package), vous obtenez un fichier JAR qu’il suffit de lancer (java -jar demo-spring-rest-docs-0.0.1-SNAPSHOT.jar). L’API est alors exposée, mais aussi la documentation, accessible sur http://localhost:8080/docs/index.html. Donc dès que l’application sera déployée quelque part, sa documentation sera présente !

Cacher par défaut les trop grands éléments

Parfois nous devons documenter des endpoints qui renvoient des réponses relativement longues. Ce qui n’est pas sans alourdir la page de documentation. Heureusement, AsciiDoc a une balise pour résoudre le problème : [%collapsible].

Tout ce qui est contenu dans un bloc ainsi annoté est caché par défaut et le lecteur doit cliquer dessus pour le déplier :

Élément trop grand (cliquez ici pour le déplier)
    [%collapsible]
    ====
    Lorem ipsum dolor sit amet, consectetur adipiscing elit.
    Phasellus eget mollis neque.
    Etiam tempor lacinia lorem eget auctor.
    Quisque accumsan leo a tincidunt hendrerit.
    Nam eros ante, scelerisque eu tempus et, vestibulum luctus turpis.
    Donec id nisi risus.
    Nullam eu purus vulputate velit pharetra hendrerit.
    Donec varius, velit vitae aliquam interdum, dui sapien faucibus ipsum, et sollicitudin ligula magna quis velit.
    Donec luctus sed nisi ac blandit.
    Phasellus sodales mattis pharetra.
    Duis dignissim tellus nibh, quis imperdiet turpis pharetra et.
    Phasellus purus odio, pulvinar vel urna vel, consequat vulputate metus.
    Nunc elementum ornare eleifend.
    Pellentesque non dapibus ipsum.
    Nunc malesuada varius elit, auctor tristique nisl pellentesque sed.
    Vestibulum justo mauris, molestie ut tincidunt a, condimentum ac turpis.
    Aliquam eu interdum orci.
    ====

C’est ainsi que dans la documentation de notre application de démonstration nous retrouvons par exemple :

.Response
[%collapsible]
====
include::{snippets}/getAllCompanies/http-response.adoc[]
====

Alléger la description des requêtes/réponses

Pour décrire la requête liée à un endpoint, rien de mieux que le snippet http-request. Celui-ci contient l’URL, le verbe HTTP, les headers et éventuellement le body.

Mais bien souvent, les headers sont nombreux. Notamment ceux liés à la sécurité. Et il en va de même pour la réponse, avec de nombreux headers ajoutés par le framework et son outillage.

Si ces headers ont un sens métier, tant mieux. Mais s’ils ne font que créer du "bruit", alors autant les enlever de la description (de la requête comme de la réponse), elle n’en sera que plus claire.

Je vous propose ainsi un petit utilitaire (ControllerTestUtils) pour éviter de répéter dans chaque test la même suppression de headers :

public class ControllerTestUtils {

    static OperationRequestPreprocessor preprocessRequest() {
        return Preprocessors.preprocessRequest(removeHeaders("Content-Length", "X-CSRF-TOKEN"), prettyPrint());
    }

    static OperationResponsePreprocessor preprocessResponse() {
        return Preprocessors.preprocessResponse(removeHeaders("Content-Length", "Pragma", "X-XSS-Protection", "Expires", "X-Frame-Options", "X-Content-Type-Options", "Cache-Control"), prettyPrint());
    }

}

Libre à vous de supprimer tel ou tel header selon vos goûts ou vos besoins.

Si l’on prend l’exemple du header Content-Type dans la requête de suppression d’une Company, il suffit de l’ajouter à la liste des headers à supprimer (cf. code ci-dessus).

Avant :
DELETE /companies/ID_1 HTTP/1.1
Content-Type: application/json;charset=UTF-8
Host: localhost:8080

{
  "id" : "ID_1",
  "name" : "CoolCorp",
  "location" : "Paris",
  "creationDate" : "2021-11-06T11:03:53.066+00:00"
}
Après :
DELETE /companies/ID_1 HTTP/1.1
Host: localhost:8080

{
  "id" : "ID_1",
  "name" : "CoolCorp",
  "location" : "Paris",
  "creationDate" : "2021-11-06T11:01:34.532+00:00"
}

Générer un fichier OpenAPI

"Hé, c’est bien joli tout ça, mais dans mon équipe on a l’habitude d’utiliser Swagger, c’est bien plus complet comme outil, on peut même lancer des requêtes depuis la page de documentation !"
— Un développeur qui a ses petites habitudes

La remarque est intéressante ! En quittant Swagger, on passe d’une page interactive à une page HTML complètement statique. On perdrait donc au change ?
Peut-être …​ mais ne parlons pas trop vite !

Allons droit au but : il est tout à fait possible de générer un fichier décrivant l’API dans un format "Swagger-compatible", autrement dit au format OpenAPI. Mais pas par défaut, nous devons ajouter une petite dépendance qui vient étendre les capacités de Spring REST Docs :

        <dependency>
            <groupId>com.epages</groupId>
            <artifactId>restdocs-api-spec-mockmvc</artifactId>
            <version>0.14.1</version>
            <scope>test</scope>
        </dependency>

Et lorsque l’on documente un endpoint, il faut désormais importer la méthode document() de cette dépendance :

import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document;

En relançant les tests, on s’aperçoit alors qu’un nouveau "snippet" est généré : resource.json.

Nouvel aperçu des fichiers générés
Nouvel aperçu des fichiers générés

Il s’agit d’un snippet au format OpenAPI. Il ne reste donc plus qu’à rassembler ces snippets en un seul fichier, un peu comme nous savons déjà le faire pour la version AsciiDoc. Mais cette fois-ci, nul besoin d’un "fichier racine", nous allons nous servir d’un plugin.

Mais …​ problème en vue ! La dépendance ajoutée ne propose qu’un plugin Gradle et notre projet utilise Maven ! Pas de panique, la communauté est vaste et prévoyante, il existe un plugin pour générer la documentation finale : restdocs-spec-maven-plugin.

En l’ajoutant dans la section "build" du fichier pom.xml, nous allons pouvoir générer un fichier au format OpenAPI en plus du fichier HTML précédent :

            <plugin>
                <groupId>io.github.berkleytechnologyservices</groupId>
                <artifactId>restdocs-spec-maven-plugin</artifactId>
                <version>0.21</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>generate</goal>
                        </goals>
                        <configuration>
                            <name>Demo Spring REST Docs</name>
                            <version>${project.version}</version>
                            <host>localhost:8080</host>
                            <outputDirectory>${project.build.directory}/generated-docs</outputDirectory>
                            <filename>openapi</filename>
                            <specification>OPENAPI_V3</specification>
                            <description>API description for Demo Spring REST Docs service</description>
                        </configuration>
                    </execution>
                </executions>
            </plugin>

De la même manière que pour le fichier HTML, comme ce fichier openapi.yml est déposé dans un répertoire exposé, il sera accessible une fois l’application lancée. Et à partir de ce moment-là, libre à vous de le fournir comme point d’entrée à une instance de Swagger UI.

Pour cela, on peut tester rapidement la qualité du fichier généré en démarrant une instance de Swagger UI :

docker run -p 80:8080 swaggerapi/swagger-ui

Une fois le container démarré, il suffit de se rendre sur http://localhost (Swagger UI fonctionnant sur le port 80 d’après notre commande ci-dessus), et de fournir l’adresse du fichier openapi.yml dans la barre de recherche en haut de la page. La documentation apparait …​ avec tout le fonctionnement habituel de Swagger. Alors, il est satisfait le développeur qui a ses petites habitudes ?

Aperçu de la documentation rendue par Swagger UI
Aperçu de la documentation rendue par Swagger UI

Alors, est-ce que vous commencez à être convaincu par Spring REST Docs ?

C’est en tout cas le cas pour ma part et je vais continuer de le déployer sur les projets sur lesquels j’ai le plaisir de travailler !


Liens utiles :