KotlinでSpring REST Docsを作る
概要
Spring REST Docsは、RESTfulサービスのAPIドキュメント作成に使用されます。APIドキュメントはテストコードに基づいて作成されます。
ここでは、Kotlin言語を利用して簡単なSpring REST Docsプロジェクトを作成します。
プロジェクト作成
次のようにcurlコマンドを使用して、Spring Bootの初期プロジェクトを作成します。
curl https://start.spring.io/starter.tgz \
-d bootVersion=2.5.6 \
-d dependencies=web,restdocs \
-d baseDir=spring-rest-docs \
-d groupId=com.devkuma \
-d artifactId=spring-rest-docs \
-d packageName=com.devkuma.rest.docs \
-d applicationName=RestDocsApplication \
-d packaging=jar \
-d language=kotlin \
-d javaVersion=11 \
-d type=gradle-project | tar -xzvf -
上記のコマンドを実行すると、Spring WebとSpring REST Docsの依存関係が追加されます。
生成されたプロジェクトの確認
プロジェクトファイル構造
正しく生成されていれば、プロジェクトのファイル構造は次のようになります。
.
├── HELP.md
├── build.gradle.kts
├── gradle
│ └── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle.kts
└── src
├── main
│ ├── kotlin
│ │ └── com
│ │ └── devkuma
│ │ └── rest
│ │ └── docs
│ │ └── RestDocsApplication.kt
│ └── resources
│ ├── application.properties
│ ├── static
│ └── templates
└── test
└── kotlin
└── com
└── devkuma
└── rest
└── docs
└── RestDocsApplicationTests.kt
ビルドスクリプト
/build.gradle.kts
// ... 省略 ...
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.springframework.restdocs:spring-restdocs-mockmvc")
}
// ... 省略 ...
依存ライブラリにSpring REST Docsライブラリ(spring-restdocs-mockmvc)がテスト用として含まれていることがわかります。
REST Docs設定
初期ビルドスクリプトエラーの修正
最初にプロジェクトをビルドすると、次のようなエラーが発生することがあります。
> Configure project :
e: /Users/kimkc/develop/tutorial/spring-tutorial/spring-rest-docs/build.gradle.kts:42:14: Unresolved reference: snippetsDir
e: /Users/kimkc/develop/tutorial/spring-tutorial/spring-rest-docs/build.gradle.kts:46:13: Unresolved reference: snippetsDir
e: /Users/kimkc/develop/tutorial/spring-tutorial/spring-rest-docs/build.gradle.kts:47:12: Unresolved reference. None of the following candidates is applicable because of receiver type mismatch:
public val NamedDomainObjectContainer<KotlinSourceSet>.test: NamedDomainObjectProvider<KotlinSourceSet> defined in org.gradle.kotlin.dsl
public val SourceSetContainer.test: NamedDomainObjectProvider<SourceSet> defined in org.gradle.kotlin.dsl
public val TaskContainer.test: TaskProvider<Test> defined in org.gradle.kotlin.dsl
エラーが発生する原因は、設定ファイルが正しく生成されていないためです。次のコードを見ると、taskではsnippetsDir変数として使用していますが、snippetsDirが正しく宣言されていません。
extra["snippetsDir"] = file("build/generated-snippets")
... 省略 ...
tasks.test {
outputs.dir(snippetsDir)
}
tasks.asciidoctor {
inputs.dir(snippetsDir)
dependsOn(test)
}
次のように変更します。
//extra["snippetsDir"] = file("build/generated-snippets")
val snippetsDir by extra { file("build/generated-snippets") } // 変数変更
... 省略 ...
tasks.test {
outputs.dir(snippetsDir)
}
tasks.asciidoctor {
inputs.dir(snippetsDir)
//dependsOn(test)
dependsOn(tasks.test) // 変更
}
再度実行すると、エラーはなくなるはずです。
ビルドスクリプトにREST Docs関連設定を追加
... 省略 ....
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.springframework.restdocs:spring-restdocs-mockmvc")
asciidoctor("org.springframework.restdocs:spring-restdocs-asciidoctor") // 1
}
... 省略 ....
tasks.asciidoctor {
inputs.dir(snippetsDir)
//dependsOn(test)
dependsOn(tasks.test) // 変更
doFirst { // 2
delete {
file("src/main/resources/static/docs")
}
}
}
tasks.register("copyHTML", Copy::class) { // 3
dependsOn(tasks.asciidoctor)
from(file("build/asciidoc/html5"))
into(file("src/main/resources/static/docs"))
}
tasks.build { // 4
dependsOn(tasks.getByName("copyHTML"))
}
tasks.bootJar { // 5
dependsOn(tasks.asciidoctor)
dependsOn(tasks.getByName("copyHTML"))
}
上記スクリプトの説明は次のとおりです。
asciidoctor依存関係設定を追加します。- この依存関係を追加しないと、ドキュメントが正しく生成されない場合があります。
asciidoctorタスクで生成されたHTMLを削除します。- 生成されたHTMLドキュメントを削除します。
copyHTMLタスクを登録します。- このタスクは
tasks.asciidoctorタスクに依存しています。 - タスクの処理として、ビルドコンテキストパス(
build/asciidoc/html5)にあるHTMLドキュメントをsrc/main/resources/static/docsへコピーします。こうすることで、実際のアプリケーション実行時に生成された仕様書を提供できます。
- このタスクは
- ビルド実行時に
copyHTMLを実行します。dependsOn関数でcopyHTMLタスクへの依存を追加しています。- ビルドタスクが実行されると、まず
copyHTMLが実行され、その後ビルドが開始されます。
- 実行可能jarをビルドするときにドキュメント生成とコピーを実行します。
bootJarタスクが実行されると、asciidoctorとcopyHTMLタスクが先に実行されます。
REST API実装
APIドキュメントを作成するために、簡単なREST APIを作ってみます。
エンティティクラス作成
/src/main/kotlin/com/devkuma/rest/docs/contrentityoller/Product.kt
package com.devkuma.rest.docs.entity
data class Product(
val code: String,
val name: String,
val price: Int
)
コントローラークラス作成
/src/main/kotlin/com/devkuma/rest/docs/controller/ProductController.kt
package com.devkuma.rest.docs.controller
import com.devkuma.rest.docs.entity.Product
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("/v1/products")
class ProductController {
private var products: List<Product> = listOf(
Product("1", "Keyboard", 2000),
Product("2", "Monitor", 3000),
Product("3", "Mouse", 1000),
)
@GetMapping("")
fun getProducts(): List<Product> {
return products
}
@GetMapping("/{code}")
fun getProduct(@PathVariable code: String): Product {
return products.first { p -> p.code.equals(code, true) }
}
}
テストコード作成
次に、APIドキュメントのためのテストコードを書いてみます。
/src/test/kotlin/com/devkuma/rest/docs/controller/ProductControllerTests.kt
package com.devkuma.rest.docs.controller
import org.hamcrest.CoreMatchers.containsString
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document
import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders
import org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath
import org.springframework.restdocs.payload.PayloadDocumentation.responseFields
import org.springframework.restdocs.request.RequestDocumentation.parameterWithName
import org.springframework.restdocs.request.RequestDocumentation.pathParameters
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import org.springframework.test.web.servlet.result.MockMvcResultMatchers
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
@WebMvcTest(ProductController::class)
@AutoConfigureRestDocs
class ProductControllerTests {
@Autowired
private lateinit var mockMvc: MockMvc
@Test
fun `GET v1-product 200 ok`() { // 1
// When
val result = mockMvc.perform(get("/v1/products/2"))
// Then
result.andExpect(status().isOk)
.andExpect(MockMvcResultMatchers.content().string(containsString("Monitor")))
}
@Test
fun `GET v1-product 200 ok document`() { // 2
// When
val result = mockMvc.perform(RestDocumentationRequestBuilders.get("/v1/products/{code}", 2))
// Then
result.andExpect(status().isOk)
.andExpect(MockMvcResultMatchers.content().string(containsString("Monitor")))
.andDo(
document(
"product/get-product-by-id",
pathParameters(
parameterWithName("code").description("Product Unique Identifier")
), responseFields(
fieldWithPath("code").description("Product Unique Identifier"),
fieldWithPath("name").description("Name of the product"),
fieldWithPath("price").description("Product Price")
)
)
)
}
}
上記コードの説明は次のとおりです。
- 単純にテストのためのテストコードです。
- テストを実行し、ドキュメントを生成するコードです。
テストコードを実行した後、スニペット生成フォルダ(build/generated-snippets)が正しく生成されたか確認してみましょう。
build/generated-snippets
└── product
└── get-product-by-id
├── curl-request.adoc
├── http-request.adoc
├── http-response.adoc
├── httpie-request.adoc
├── path-parameters.adoc
├── request-body.adoc
├── response-body.adoc
└── response-fields.adoc
テスト実行時にエラーが発生した場合はGradleバージョンを確認
上記のビルドスクリプトに設定を追加した後、ビルドを実行すると次のようなエラーが発生する場合があります。
... 省略 ...
FAILURE: Build failed with an exception.
* What went wrong:
Some problems were found with the configuration of task ':asciidoctor' (type 'AsciidoctorTask').
- In plugin 'org.asciidoctor.convert' type 'org.asciidoctor.gradle.AsciidoctorTask' method 'asGemPath()' should not be annotated with: @Optional, @InputDirectory.
... 省略 ...
これはGradle 7バージョンで発生する問題です。回避方法はありますが複雑なのでおすすめしません。今後正式に解決された後で、バージョンアップを検討するのがよさそうです。
Gradleバージョンを変更する方法は次のとおりです。
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
#distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.9.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
AsciiDoc作成
AsciiDoc文書は.adocという拡張子を使用し、AsciiDoc文法に従います。
/src/docs/asciidoc/index.adoc
=== Get Product By Id
Obtains a specific products by its unique identifier.
==== Sample Request
include::{snippets}/product/get-product-by-id/http-request.adoc[]
==== Sample Response
include::{snippets}/product/get-product-by-id/http-response.adoc[]
==== CURL sample
include::{snippets}/product/get-product-by-id/curl-request.adoc[]
==== Path Parameters
include::{snippets}/product/get-product-by-id/path-parameters.adoc[]
==== Response Fields
include::{snippets}/product/get-product-by-id/response-fields.adoc[]
asciidoctor依存関係の確認
もしビルドを実行して次のようなエラーが発生し、
> Task :asciidoctor
asciidoctor: WARNING: dropping line containing reference to missing attribute: snippets
HTMLドキュメントが正しく生成されない場合は、

依存関係にspring-restdocs-asciidoctor設定が正しく追加されているか確認してみましょう。
dependencies {
... 省略 ...
asciidoctor("org.springframework.restdocs:spring-restdocs-asciidoctor") // 1
}
Gitを使用する場合
生成されたHTMLドキュメントはバージョン管理で管理する必要がないため、管理対象ファイルから除外しましょう。
.gitignoreファイルに次のように除外フォルダを追加すればよいです。
**/src/main/resources/static/docs/
最終確認
最終的にドキュメントが正しく生成されていれば、次のようなドキュメントを確認できます。

まとめ
上記のすべてのソースコードはGitHubで確認できます。
参考
- JavadevJournal | Spring REST Docs
- Asciidoctor Gradle Plugin Suite 3.1.0
- 私の初めてのSpringRestDocs適用記 part 1
- Spring REST Docs基本設定とAPI文書を作ってみる