Generating Custom Templated Snippets with Spring REST Docs
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 ofsuper
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
— aString
denoting the name of the input template and output snippet files, minus extensionsattributes
— aMap
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
ordescription.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) orMap.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.