(Первоначальный автор этого сообщения в блоге: Роман Тучин)
Идея иметь документацию в виде кода не нова и была внедрена давно. Одним из пионеров был LaTeX. Описанный здесь подход — один из многих удобных способов документирования API, при котором вы можете свести к минимуму ручное написание документации. Spring REST Docs — отличный инструмент для этого.
Отказ от ответственности
В этом сообщении в блоге не рассматриваются основы использования Spring REST Docs. В Интернете вы можете найти несколько руководств по началу работы с ним, т.е. этот или этот. Цель этой записи в блоге — дать больше информации о расширенной функции, а именно — о создании пользовательских фрагментов документации, основанных на вашем коде. Например, фрагмент может содержать таблицу со всеми возможными значениями перечисления поля в полезной нагрузке запроса для RESTful API.
Технический стек
В качестве примеров технологий я выбрал следующие:
- Язык программирования Котлин.
- Фреймворк Ktor для реализации примера RESTful API
- Фреймворк Koin для внедрения зависимостей
- JUnit 5 для запуска тестов
- REST Assured совместно с Spring REST Docs для генерации документации
- Грэдл (Котлин) для строительства
Обзор проекта
Исходный код проекта можно найти здесь.
В качестве примера проекта я подготовил простой Contact API для создания контактов.
Он имеет одну конечную точку для создания контактов:
POST /contacts HTTP/1.1
{
"contactKey" : "dc4b7722-5c47-4ee5-939e-6469b1f9d4e7",
"type" : "END_USER"
}
который обрабатывает запрос и возвращает другую полезную нагрузку JSON:
HTTP/1.1 200 OK Content-Type: application/json; charset=UTF-8
{ "contactKey" : "8fa5c51c-cb3e-4cf3-a4b5-6730c99cb011" }
Сначала я подготовил Ktor-Application
:
fun main(args: Array<String>) { // Start Ktor embeddedServer(Netty, commandLineEnvironment(args)).start() }
fun Application.main() { install(Koin) { modules(dependencies) } install(DefaultHeaders) { } install(CallLogging) { level = Level.DEBUG }
install(ContentNegotiation) { jackson { // Configure Jackson's ObjectMapper here } }
val createContactHandler: CreateContactHandler by inject()
routing { post("/contacts/") { createContactHandler.handle(call) } } }
Здесь вы видите определенный POST
-маршрут /contacts/
для создания нового контакта. Вызов маршрута передается CreateContactHandler
(введен Koin
):
class CreateContactHandler { companion object : KLogging()
suspend fun handle(call: ApplicationCall) { val request = call.receive(CreateContactRequest::class) logger.debug { "handling create contact request: $request" } //do handling val response = CreateContactResponse(UUID.randomUUID().toString()) call.respond(response) } }
data class CreateContactRequest( val contactKey: String, val type: ContactType )
data class CreateContactResponse( val contactKey: String )
Обработчик принимает входящий запрос, десериализует полезную нагрузку JSON, обрабатывает ее и отвечает другой полезной нагрузкой JSON.
Генерация документации
Для создания документации я использую следующий модульный тест:
@ExtendWith(RestDocumentationExtension::class) class ContactApiDocTest { private val embeddedServer = embeddedServer(factory = Netty, port = 8080, module = Application::main) .apply { start() }
lateinit var spec: RequestSpecification
@BeforeEach fun setup(restDocumentation: RestDocumentationContextProvider) { this.spec = RequestSpecBuilder() .addFilter( RestAssuredRestDocumentation.documentationConfiguration(restDocumentation) .operationPreprocessors() .withRequestDefaults( Preprocessors.prettyPrint() ) .withResponseDefaults( Preprocessors.prettyPrint() ) .and() ) .build() }
@Test fun `should generate docs`() { given(this.spec) .header(HttpHeaders.CONTENT_TYPE, "application/json") .filter(document("create-contact")) .body(CreateContactRequest(contactKey = UUID.randomUUID().toString(), type = ContactType.END_USER)) .`when`() .port(8080) .post("/contacts") .then() .assertThat() .statusCode(200) .header(HttpHeaders.CONTENT_TYPE, "application/json; charset=UTF-8") } }
Код представляет собой типичный тест Spring REST Docs
, который запускает встроенный веб-сервер на фиксированном порту 8080
и отправляет ему запрос. В реальном сценарии вы должны использовать случайный номер порта. Благодаря RestDocumentationExtension
и соответствующей конфигурации в setup()
стандартные сниппеты генерируются в папку build/generated snippets
:
✔ 18:37 ~/projects/contactapi/build/generated-snippets/create-contact $ ll
total 56
Aug 12 14:17 .
Aug 9 17:28 ..
Aug 12 14:21 contact-types.adoc
Aug 12 14:21 curl-request.adoc
Aug 12 14:21 http-request.adoc
Aug 12 14:21 http-response.adoc
Aug 12 14:21 httpie-request.adoc
Aug 12 14:21 request-body.adoc
Aug 12 14:21 response-body.adoc
Позже вам нужно поместить файл index.adoc
в ваш src/docs/asciidoc
. Это ваша отправная точка для написания документов. Для начала добавьте следующий текст:
= Contact API Kreuzwerker Author; :doctype: book :icons: font :source-highlighter: highlightjs :toc: left :sectnums:
== Intro Contact API is for managing of contacts.
=== Request include::{snippets}/create-contact/curl-request.adoc[]
include::{snippets}/create-contact/http-request.adoc[]
=== Response include::{snippets}/create-contact/response-body.adoc[]
include::{snippets}/create-contact/http-response.adoc[]
Как видите, я включил несколько фрагментов, созданных на предыдущих шагах.
Теперь вы можете начать генерацию документов с помощью gradle
:
~/projects/contactapi [master] $ ./gradlew asciidoctor
После успешной генерации откройте его из build/asciidoc/html5/index.html
. Это выглядит как:
Теперь у вас есть обзор стандартных функций Spring REST Docs
, и мы можем продолжить…
Пользовательские фрагменты документов на основе вашего кода
Как вы видели выше, наш CreateContactRequest
имеет поле типа ContactType
. Класс перечисления ContactType
is:
enum class ContactType {
ADMIN,
END_USER,
TESTER
}
и я хотел бы задокументировать возможные значения поля, которые можно передать в качестве полезной нагрузки запроса. Скажем, в виде таблицы. Для этого вам следует создать собственный фрагмент для Spring REST Docs
.
Первый шаг — добавить класс, расширяющий TemplatedSnippet
:
class ContactTypesSnippet(snippetName: String?, attributes: MutableMap<String, Any>?) : TemplatedSnippet(snippetName, attributes) { companion object { const val SNIPPET_NAME = "contact-types" }
constructor() : this(SNIPPET_NAME, null)
override fun createModel(operation: Operation?): Map<String, Any> { return mapOf( "contactTypes" to ContactType.values() ) } }
fun contactTypesSnippet() = ContactTypesSnippet()
Вы видите, что в методе createModel
вы добавляете массив всех значений из ContactType
enum под именем contactTypes
Во-вторых, вы настраиваете дополнительный пользовательский фрагмент в модульном тесте с помощьюand().snippets().withAdditionalDefaults(contactTypesSnippet())
@BeforeEach
fun setup(restDocumentation: RestDocumentationContextProvider) {
this.spec = RequestSpecBuilder()
.addFilter(
RestAssuredRestDocumentation.documentationConfiguration(restDocumentation)
.operationPreprocessors()
.withRequestDefaults(
Preprocessors.prettyPrint()
)
.withResponseDefaults(
Preprocessors.prettyPrint()
)
.and()
.snippets().withAdditionalDefaults(contactTypesSnippet())
)
.build()
}
После этого необходимо добавить шаблон Mustache
. Поместите под src/test/resources/org/springframework/restdocs/templates/asciidoctor/
следующий contact-types.snippet
файл:
=== Contact types
|===
| Type name | Description
{{#contactTypes}}
| {{name}}
| some description
{{/contactTypes}}
|===
Вы видите новый раздел во фрагменте, который мы только что определили, и перебираете массив значений перечисления contactTypes.
Столбец Description
является статическим, но он также может быть легко предоставлен перечислением.
Последний шаг, вам нужно включить этот фрагмент в свой index.adoc
, добавив:
include::{snippets}/create-contact/contact-types.adoc[]
Теперь снова запустите $ ./gradle asciidoctor
и в результате вы увидите следующий раздел в документах:
Вывод
Как вы видели, вы можете создавать отличную документацию, полностью полагаясь на свою кодовую базу, а это означает, что позже вам не нужно будет синхронизировать документы и код, и вы можете просто публиковать документы каждый раз, когда вы создаете свой проект (т.е. через Jenkins
).
Кредиты на обложку идут по адресу: pexels.com