-
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 generateDocs
Find 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 theDocumentedJSONField
objects. -
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:
andDocument
andDocumentErrorResponse
andDocumentEmptyObjectResponse
andDocumentCustomRequestSchema
andDocumentCustomRequestSchemaErrorResponse
andDocumentCustomRequestSchemaEmptyObjectResponse
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