-
Notifications
You must be signed in to change notification settings - Fork 0
API Documentation
API documentation is generated through the use of the Spring REST Docs API specification Integration (aka restdocs-api-spec), a Spring Rest Docs extension that builds an OpenAPI specification or a Postman collection from its description, included in the controller tests. To see examples of how to document the API, hop to one of the controller tests.
- Generating the API specification
- How to document the API
- Note about the state of the documentation framework
Run the generateDocs gradle task to generate the OpenAPI specification or the Postman collection.
Run the following command in your shell:
./gradlew generateDocsFind the OpenAPI specification and Postman collection under docs/ after running the task.
As mentioned above, the API documentation is generated through the use of descriptions embedded in the controller tests. To make documentation easier and more consistent, some helper classes and methods have been created to wrap the underlying restdocs-api-spec and Spring Rest Docs.
These classes ensure that the documentation is consistent and structured and that it is generated only once for each payload. They also make documenting array variations of the same payload a lot easier.
If you have any doubts, feel free to check the controller tests for examples.
Tags are used to group the API endpoints in the generated specification. To add a new tag, add a mapping to the tag-descriptions.yaml file in the src/test/kotlin/pt/up/fe/ni/website/backend/utils/documentation folder. The mapping should be in the form of <tag name>: <tag description>, where the tag name is the name of the tag and the tag description is a short description of the tag.
Events: Endpoints and operations for event management.Then update the Tag enum in the same folder to include the new tag. Note that the fullName property of the enum should match the tag name in the tag-descriptions.yaml file.
enum class Tag(override val fullName: String) : ITag {
AUTH("Authentication"),
ACCOUNT("Accounts"),
EVENT("Events"),
GENERATION("Generations"),
POST("Posts"),
PROJECT("Projects")
}JSON payloads are documented using the PayloadSchema helper class.
This class generates a payload resprentation, associating the documented fields representing the payload and a schema name, ensuring that the schema is only generated once.
PayloadSchema objects are generally used as properties of ModelDocumentation's subclass objects, but can also be required for some methods.
The ModelDocumentation helper class is used to link a payload to a documentation tag, serving as the context for the documentation of a payload.
JSON fields are documented using the DocumentedJSONField helper class.
This class keeps track of field properties, dynamically manipulating them as required by the documentation context.
These properties are:
| Property | Description | Default value |
|---|---|---|
path |
The name of the field. | |
description |
The description of the field. | null |
type |
The type of the field. | null |
optional |
Whether the field is optional. | false |
ignored |
Whether the field should be ignored. | false |
attributes |
A map of attributes to be added to the field. | emptyMap() |
isInRequest |
Whether the field is in the request payload. | true |
isInResponse |
Whether the field is in the response payload. | true |
Example:
DocumentedJSONField("id", "Account ID", JsonFieldType.NUMBER, isInRequest = false)The DocumentedJSONField class provides the getFieldDescriptor method to build a FieldDescriptor object from the defined properties, used by the underlying documentation framework.
The class also introduces a method extending MutableList, MutableList<DocumentedJSONField>.addFieldsBeneathPath, wich adds a list of fields as children of the field with the given path. It takes the following parameters:
| Parameter | Description | Default value |
|---|---|---|
path |
The path of the field to add the children to. | |
documentedJSONFields |
The list of fields to add as children. | |
optional |
Whether the fields are optional. | false |
ignored |
Whether the fields should be ignored. | false |
addRequest |
Whether the fields should be added to the request payload. | false |
addResponse |
Whether the fields should be added to the response payload. | false |
Note that the addRequest and addResponse parameters default to false, so you should explicitly set them to true if you want the fields to be added to the request or response payload.
To directly document a payload, use the PayloadSchema class with a schemaName and a list of DocumentedJSONField objects.
Example:
val passwordChangePayload = PayloadSchema(
"password-change",
mutableListOf(
DocumentedJSONField(
"oldPassword",
"Current account password",
JsonFieldType.STRING
),
DocumentedJSONField(
"newPassword",
"New account password",
JsonFieldType.STRING
)
)
)Direct documentation of payloads is useful for some of the documentation methods, but it is only necessary to do so if the payload is used as the request of a different payload, wrapped in the ModelDocumentation class.
The PayloadDocumentation class provides the following methods:
-
[Request|Response]().schema()- Returns the payload schema for the request or response. -
[Request|Response]().getSchemaFieldDescriptors()- Returns the field descriptors for the request or response, building them from theDocumentedJSONFieldobjects. -
getPayloadArraySchema()- Returns a payload schema for an array of the payload.
The ModelDocumentation class is used to document a payload model, associating it with a tag and a name. Models are supposed to inherit from this class and to add the fields they need to document.
Example:
class PayloadProject : ModelDocumentation(
Tag.PROJECT.name.lowercase(),
Tag.PROJECT,
mutableListOf(
DocumentedJSONField("title", "Project title", JsonFieldType.STRING),
DocumentedJSONField("description", "Project description", JsonFieldType.STRING),
DocumentedJSONField("isArchived", "If the project is no longer maintained", JsonFieldType.BOOLEAN),
DocumentedJSONField(
"technologies",
"Array of technologies used in the project",
JsonFieldType.ARRAY,
optional = true
),
DocumentedJSONField("technologies.*", "Technology", JsonFieldType.STRING, optional = true),
DocumentedJSONField(
"slug",
"Short and friendly textual event identifier",
JsonFieldType.STRING,
optional = true
),
DocumentedJSONField("id", "Project ID", JsonFieldType.NUMBER, isInRequest = false),
DocumentedJSONField(
"teamMembers",
"Array of members associated with the project",
JsonFieldType.ARRAY,
isInRequest = false
),
DocumentedJSONField(
"teamMembersIds",
"Array with IDs of members associated with the project",
JsonFieldType.ARRAY,
optional = true,
isInResponse = false
),
DocumentedJSONField(
"teamMembersIds.*",
"Account ID",
JsonFieldType.NUMBER,
optional = true,
isInResponse = false
)
).addFieldsBeneathPath(
"teamMembers[]",
PayloadAccount().payload.documentedJSONFields,
addResponse = true,
optional = true
)
)This class provides a getModelDocumentationArray method that generates a ModelDocumentation object containing an array of the payload, with the same tag.
To document an endpoint, set up the right ModelDocumentation subclass object and use the andDocument MockMVC extension method and its variants using the MockMVC result actions.
andDocument and variants parameters are:
| Parameter | Description | Default value |
|---|---|---|
| documentation | The ModelDocumentation subclass object to use. |
|
| summary | The summary of the endpoint. | null |
| description | The description of the endpoint. | null |
| requestHeaders | A list of HeaderDescriptorWithType objects to document. |
emptyList() |
| responseHeaders | A list of HeaderDescriptorWithType objects to document. |
emptyList() |
| urlParameters | A list of ParameterDescriptorWithType objects to document. |
emptyList() |
| documentRequestPayload | Whether the request payload should be documented. | false |
| hasRequestPayload | Whether the request has a payload. This associates the correct schema with the request, but doesn't document it. Intended to be used with "fail" tests | false |
andDocument method variants:
andDocumentandDocumentErrorResponseandDocumentEmptyObjectResponseandDocumentCustomRequestSchemaandDocumentCustomRequestSchemaErrorResponseandDocumentCustomRequestSchemaEmptyObjectResponse
The "custom" variants allow to document a custom request schema, instead of the one associated with the ModelDocumentation object. They use an extra parameter:
| Parameter | Description | Default value |
|---|---|---|
| requestSchema | The PayloadSchema object to use. |
// ...
val documentation: ModelDocumentation = PayloadProject()
// ...
@Test
fun `should return all projects`() {
mockMvc.perform(get("/projects"))
.andExpectAll(
status().isOk,
content().contentType(MediaType.APPLICATION_JSON),
content().json(objectMapper.writeValueAsString(testProjects))
)
.andDocument(
documentation.getModelDocumentationArray(),
"Get all the projects",
"The operation returns an array of projects, allowinto easily retrieve all the created " +
"projects. This is useful for example in thfrontend project page, " +
"where projects are displayed."
)
}We are building our documentation on top of the Spring REST Docs framework, which is a great tool for documenting REST APIs. However, it is not flexible and easy to use enough for our needs. restdocs-api-spec is added on top of Spring REST Docs making the documentation process even more limiting and difficult to use.
That's what justifies the creation of all the helper methods and classes in this project, to make the documentation process easier and more flexible, less taunting for new developers and to provide an overall better documetation experience. Bringing the documentation closer to the code and to do it in a Test Driven Development (TDD) approach are main goals of the endpoint documentation of this project, so the documentation is always up to date and accurate, and the documentation process is as easy as possible, saving time and trouble in the long run.
Don't think that tools (Spring REST Docs, restdocs-api-spec) are the problem, because they are quite powerful, the problem lies in the needed interfaces and boilerplate code to use them. Keep in mind that there is a lot more functionality in this tools that isn't exposed by the current state of our documentation framework and that if you find useful methods you think are worth the trouble you should get our attention to them
To learn about the availabe documentation classes and methods read the Documenting your API section of the Spring REST Docs documentation and the Usage with Spring Rest Docs section of the restdocs-api-spec documentation. Keep in mind that most of the examples assume the usage of Spring boot with Java, but the same concepts apply to Spring with Kotlin.
JSON payloads are documented using arrays of FieldDescriptor objects.
These are used in the context of the resource.builder method with the requestFields and responseFields as parameters, accompanied by the respective com.epages.restdocs.apispec.Schema object, that names the payload schema.
These objects are created using the fieldWithPath method, which takes as parameter the path to the field as described in JSON Field Paths. Then the resulting object is modified using the available methods to add the desired documentation to the field. The field description is added setting the description property of the object (e.g. fieldWithPath("name").description("The name of the event"))
// ...
val documentedFields = listOf<FieldDescriptor>(
fieldWithPath("id").type(JsonFieldType.NUMBER).description("Event ID"),
fieldWithPath("title").type(JsonFieldType.STRING).description("Event title"),
fieldWithPath("description").type(JsonFieldType.STRING).description("Event description"),
fieldWithPath("registerUrl").type(JsonFieldType.STRING).description("Link to the event registration").optional(),
fieldWithPath("dateInterval.startDate").type(JsonFieldType.STRING).description("Event beginning date"),
fieldWithPath("dateInterval.endDate").type(JsonFieldType.STRING).description("Event finishing date").optional(),
fieldWithPath("location").type(JsonFieldType.STRING).description("Location for the event").optional(),
fieldWithPath("category").type(JsonFieldType.STRING).description("Event category").optional(),
fieldWithPath("thumbnailPath").type(JsonFieldType.STRING).description("Path for the event thumbnail image"),
fieldWithPath("associatedRoles[]").description("Array of Roles/Activity associations"),
fieldWithPath("associatedRoles[].*.role").type(JsonFieldType.OBJECT).description("Roles associated with the activity").optional(),
fieldWithPath("associatedRoles[].*.activity").type(JsonFieldType.OBJECT).description("An activity that aggregates members with different roles").optional(),
fieldWithPath("associatedRoles[].*.permissions").type(JsonFieldType.OBJECT).description("Permissions of someone with a given role for this activity").optional(),
fieldWithPath("associatedRoles[].*.id").type(JsonFieldType.NUMBER).description("Id of the role/activity association").optional(),
)
mockMvc.perform(get("/events/{id}", testEvent.id))
.andDo(
document(
"events/{ClassName}/{methodName}",
snippets = arrayOf(
resource(
builder()
.summary("Get events by ID")
.description(
"""
This endpoint allows the retrieval of a single event using its ID.
It might be used to generate the specific event page.
""".trimIndent(),
)
.pathParameters(parameterWithName("id").description("ID of the event to retrieve"))
.responseSchema("event-response")
.responseFields(documentedFields)
.tag("Events")
.build(),
),
),
),
)
// ...Getting Started
Architecture Details
Implementation Details
Testing
Documentation
Deployment