Generating Custom Templated Snippets with Spring REST Docs

Reuben Frankel
6 min readSep 2, 2020

Spring REST Docs is a powerful tool used to automatically generate documentation for a RESTful service. Here at Matatika, we use Spring REST Docs to document our API endpoints by generating code snippets during the system integration test (SIT) stage of our continuous integration/continuous deployment pipeline. Using this workflow, we can guarantee that our code snippets are functionally maintained and up-to-date at all times.

Recently, we released our documentation publicly. A key feature of this is a Postman collection containing requests to the available endpoints of the Matatika API. Using a forked and modified version of restdocs-to-postman, we define a description in each of our API SITs, generate a description snippet and append to its corresponding Postman request. This article aims to cover how we create these custom description snippets, as well as what features we may implement with custom snippets in the future.

Prerequisites

  • Project with tests configured with Spring REST Docs — refer to the Spring REST Docs docs for more information
  • Basic Java understanding

Our Tools

  • Java 8
  • JUnit 4
  • REST Assured 3

Custom Snippet Templates

The default snippets generated by Spring REST docs are formed from templates. This is to say that although the content of each generated snippet is context-dependent, they all follow a similar structure.

For example, here is the content of the cURL request template:

```bash$ curl {{url}} {{options}}```

This is Mustache template syntax. The basic premise here is values for the {{url}} and {{options}} tags will be substituted in when a test is run.

  • {{url}} will be the accessed URL
  • {{options}} will form the cURL options — such as HTTP method, headers or request body data

Spring REST Docs provides the functionality to override default templates, should you need to

While the Mustache syntax extends much further than the concept of tags, just this understanding is enough to begin creating custom snippets. Implementing a description snippet is slightly different to the cURL snippet outlined previously, as the content of a description is not necessarily dependent on the request operation itself (i.e. we don’t need access to the HTTP method, headers or body data). Rather, we simply want to declare a custom description in each test and have it output as a snippet.

To begin, we need to create a custom snippet template in one of the following directory paths. The directory you choose depends on whether your documentation specification is set to generate AsciiDoc (default) or Markdown snippets. If the path doesn’t exist, create the directories as appropriate:

  • ${project.basedir}/src/test/resources/org/springframework/restdocs/templates/asciidoctor
  • ${project.basedir}/src/test/resources/org/springframework/restdocs/templates/markdown

${project.basedir} is the base directory of your project

To create a custom description snippet template, first create a file and name it description.snippet. Open the file for editing and insert the Mustache tag {{text}}.

The key string enclosed by the two curly brace sets can actually be set to anything you like — if you wish to change the key, be aware that we will be referring to it as text throughout this guide

The complete content of description.snippet should be:

{{text}}

The next step is to create a class for our custom snippet so that it can be interpreted by the documentation specification.

Custom Templated Snippet Classes

In order to generate custom description snippets, we first need to create a new class DescriptionSnippet that extends TemplatedSnippet. Add a constructor that calls one of the superclass constructors, and the unimplemented createModel method:

package com.matatika.sit.api.customsnippets;

import java.util.Collections;
import java.util.Map;

import org.springframework.restdocs.operation.Operation;
import org.springframework.restdocs.snippet.TemplatedSnippet;

public class DescriptionSnippet extends TemplatedSnippet {

public DescriptionSnippet(String snippetName, Map<String, Object> attributes) {
super(snippetName, attributes);
// TODO Auto-generated constructor stub
}

@Override
protected Map<String, Object> createModel(Operation operation) {
// TODO Auto-generated method stub
return null;
}

}

There are two constructors implementations within TemplatedSnippet — the use of super in this example calls the constructor that sets the same name for both the template and generated snippets; the other allows you to specify alternate names (see here for more information)

We call the constructor of the superclass TemplatedSnippet with two arguments:

  • snippetName— a String denoting the name of the input template and output snippet files, minus extensions
  • attributes — a Map interface containing key-value pairs to populate the template tags

Since our template name will not need to be changed after we initially create the file, we can remove the need to pass this value in via the DescriptionSnippet constructor and set it directly in the super call:

This will look for a `description.snippet` template and output a description.adoc or description.md snippet, depending on your configuration

Importantly, we need to consider the source of the data we want to populate the template tags with. This is fairly trivial in the case of a description snippet, as we will be defining our description text within each test. Since this is all the data that the snippet will require, attributes will have one key-value pair:

As the only attribute we have — ”text” — is fixed, we can instantiate and initialise an immutable Map directly in super to prevent code repetition. This averts the need to declare a Map interface object with the same key each time we create a DescriptionSnippet object. In doing this, the constructor now only depends on a single description text String to create a DescriptionSnippet instance:

  public DescriptionSnippet(String description) {
super("description", Collections.singletonMap("text", description));
}

If you have more than one attribute and are using Java 8, try using Collections.toMap

If you are using Java 9 or above, you can use the methods Map.of (size-constrained) or Map.ofEntries to create and initialise immutable maps

The last step here is to return the operation attributes in createModel so that the snippet can be created from the template and supplied description attribute:

  @Override
protected Map<String, Object> createModel(Operation operation) {
return operation.getAttributes();
}

Our final class should look as follows:

package com.matatika.sit.api.customsnippets;

import java.util.Collections;
import java.util.Map;

import org.springframework.restdocs.operation.Operation
import org.springframework.restdocs.snippet.TemplatedSnippet;

public class DescriptionSnippet extends TemplatedSnippet {

public DescriptionSnippet(String description) {
super("description", Collections.singletonMap("text", description));
}

@Override
protected Map<String, Object> createModel(Operation operation) {
return operation.getAttributes();
}

}

Next, we will configure the documentation functionality of a test to handle our new custom snippet.

Test Documentation Configuration

Custom snippets are not generated by default, and need to be added to the Spring REST Docs documentation specification.

Your test should look something like this (we are using REST Assured):

package com.matatika.sit.api;

import org.junit.Test;

import com.matatika.sit.common.APITestBase;

public class WorkspacesITCase {

@Test
// view all workspaces
public void whenRequestGET_ViewAllWorkspaces_200() {
given(documentationSpec).filter(document("workspaces/view-all-workspaces"))
.when().get("http://localhost:8080/api/workspaces")
.then().assertThat().statusCode(200);
}

}

With this configuration, Spring REST Docs will generate the six default snippets for this request. To add our custom description snippet, we first need to create create an instance of the DescriptionSnippet class, and then add it as an additional argument to the document method

package com.matatika.sit.api;

import org.junit.Test;

import com.matatika.sit.api.customsnippets.DescriptionSnippet;
import com.matatika.sit.common.APITestBase;

public class WorkspacesITCase {

DescriptionSnippet descriptionSnippet = new DescriptionSnippet("Returns all workspaces for the authenticated profile.");

@Test
// view all workspaces
public void whenRequestGET_ViewAllWorkspaces_200() {
given(documentationSpec).filter(document("workspaces/view-all-workspaces", descriptionSnippet))
.when().get("http://localhost:8080/api/workspaces")
.then().assertThat().statusCode(200);
}

}

..and that’s it! Just run the test and you should see the description.adoc or description.md file appear in the specified directory.

What next?

As the Matatika brand grows, we expect more developers from different backgrounds to become involved with our service. As such, it would be beneficial for us to provide code snippets in a variety of programming languages, to make development with the Matatika service as accessible as possible.

Generating implementation examples like this will require access to operation properties such as the request URL, headers and HTTP method used. This is possible by creating an attribute model in the overridden createModel method through the operation object, rather than directly in the constructor as previously done in this guide.

--

--