SpringBoot 2.0とapiDocを連携させてみた


SpringFrameworkでAPIドキュメントを準備する手段としてSpringFoxやSpring REST Docsなどが候補にあがる。

SpringFox by springfox

Spring REST Docs

両者とも導入敷居が低くSpringFoxはコントローラクラスやレスポンスモデルにアノテーションをつけていきSpringを起動すればドキュメントエンドポイントが生まれる。APIの開発序盤からDocsを整理していけるスピード感がある。対してSpring REST DocsはテストコードにDocs生成手段を仕込みHttpStatus:200 OKHttpStatus:404 NotFoundなどレスポンス毎にDocsを整理できるので網羅的なドキュメントを整理することができる。

Spring Boot 2.0でも同様にSpringFoxやSpring REST Docsを使いAPIドキュメントを整理したかったが現時点では未対応な状況である。(2017/09/29)


何か別の手段でと思いを馳せて居たところで同僚にapiDocを教えてもらった。

apiDoc - Inline Documentation for RESTful web APIs

apiDocはJavadoc-Styleやコメントコードを駆使してAPIドキュメントをまとめapidocのコマンドを叩けばドキュメント一式のHTMLが生成される。詳細は公式を確認いただきたい。

http://apidocjs.com/#run

コメントコードが生成できればドキュメントHTMLが生成される仕組みなのでSpring REST DocsのようにテストコードにapiDoc-Params(コメントコード)を生成する仕組みを取り入れてみた。

ここからは開発したテストコードを元にドキュメントHTMLが生成されるまでをまとめていきたい。

apiDocのコメントコードを吐くテストコード

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
 * @api {GET} /api/task/:taskId タスクの取得
 * @apiName GetTask
 * @apiGroup Task
 * @apiVersion 1.0.0
 *
 * @apiUse GetTaskOk
 * @apiUse GetTaskNotFound
 *
 */
@Test
fun `GET Task`() {

    val define = DefineBuilder({
        version { "1.0.0" }
        name { "GetTaskOk" }
    })

    // @apiParamのコメントコードを吐く
    define.param { ApiParam("taskId", "タスクID", "23445", false, "7") }

    // mock
    `when`(taskHandler.fetchByTaskId(any())).thenReturn(ok().json().body(Mono.just(mockModel)))

    client.get().uri("/api/task/1")
            .accept(MediaType.APPLICATION_JSON_UTF8)
            .exchange().expectStatus().isOk
            .expectBody()
            .consumeWith {
                val actual: TaskModel = mapper.readValue(it.responseBody)
                actual.id shouldBe 1L
                actual.title shouldBe "task title"
                actual.createdAt shouldBe "2017-06-13T16:22:52Z"
                actual.updatedAt shouldBe "2017-06-13T16:22:52Z"

                // @apiSuccessのコメントコードを吐く
                define.success { actual }
                // @apiSuccessExampleのコメントコードを吐く
                define.successExample { ApiSuccessExample("Success", HttpStatus.OK, actual) }
            }

    define.genDoc()
}

タスクを1件取得するエンドポイントのテストコードに仕込んだ。最初のコメントコードでapiDocの仕様に基づきAPIの概要をまとめている。@apiUseについては後述する。
テストコードのブロック内ではDefineBuilderのインスタンスからDocをビルドアップしている。このDefineBuilderをお手製でつくってみた。

1
define.success { actual }

このsuccessに渡るインスタンスクラスのプロパティに次のような@ApiDocPropertyがついていれば@apiSuccess {String} firstname Firstname of the User.のようなコメントコードが吐かれるようにした。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
data class TaskModel(
        @ApiDocProperty(value = "タスクID", example = "23445")
        val id: Long,
        @ApiDocProperty(value = "タイトル", example = "牛乳を買う")
        val title: String,
        @ApiDocProperty(value = "完了日時", example = "yyyy-MM-dd'T'HH:mm:ss'Z'")
        val finishedAt: String?,
        @ApiDocProperty(value = "作成日時", example = "yyyy-MM-dd'T'HH:mm:ss'Z'", nullable = true)
        val createdAt: String,
        @ApiDocProperty(value = "更新日時", example = "yyyy-MM-dd'T'HH:mm:ss'Z'")
        val updatedAt: String)

上記のインスタンスは次のようなコメントコードを吐く。JSONレスポンスの各項目にも名称やサンプル値などが整理できる。

1
2
3
4
5
@apiSuccess {String} createdAt 作成日時, Nullable:TRUE, Example:yyyy-MM-dd'T'HH:mm:ss'Z'
@apiSuccess {String} finishedAt 完了日時, Nullable:FALSE, Example:yyyy-MM-dd'T'HH:mm:ss'Z'
@apiSuccess {long} id タスクID, Nullable:FALSE, Example:23445
@apiSuccess {String} title タイトル, Nullable:FALSE, Example:牛乳を買う
@apiSuccess {String} updatedAt 更新日時, Nullable:FALSE, Example:yyyy-MM-dd'T'HH:mm:ss'Z'

そして最後に@apiUseである。冒頭のコメントコードは次のようになっていた。

1
2
@apiUse GetTaskOk
@apiUse GetTaskNotFound

apiDocの特徴的な機能で@apiUseに宣言された変数は@apiDefineに宣言された変数と紐づくことになる。GetTaskOkは正常系のテストコードに紐づき、GetTaskNotFoundは404エラーになるテストコードに紐付いている。404エラーのテストコードは次のようになっている。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
fun `GET Task NotFound`() {

    val define = DefineBuilder({
        version { "1.0.0" }
        name { "GetTaskNotFound" } // `@apiDefine GetTaskNotFound`を吐く
    })

    // mock
    `when`(taskHandler.fetchByTaskId(any())).thenThrow(WebAppException.NotFoundException("task notfound."))

    client.get().uri("/api/task/1")
            .accept(MediaType.APPLICATION_JSON_UTF8)
            .exchange().expectStatus().isNotFound
            .expectBody()
            .consumeWith {
                val actual: ErrorItem = mapper.readValue(it.responseBody)

                define.error { actual }
                define.errorExample { ApiErrorExample("BadRequest", HttpStatus.BAD_REQUEST, actual) }
            }

    define.genDoc()
}

@apiUse@apiDefineの仕組みをつかえば複数のAPIレスポンスをドキュメント化することができる。
エンドポイントのあらゆる異常系を書き@apiUse@apiDefineで紐付けることでドキュメントが整備されていく仕掛けとなる。Spring REST Docsのエッセンスを上手く取り入れられた。

まとめ

apiDocが生成したドキュメントの画面キャプチャは次のようになった。

正常系

エラー


SpringFoxやSpring REST DocのSpring Boot 2.0.0(Spring5)のサポートを待つ間の暫定対応としてapiDocを連携させてみたが、思いの外ドキュメントにする手間もなくドキュメント化が実現できた。

サポートが完了したSpringFoxやSpring REST Docの新しい機能に期待をしながら、今回つくったDefineBuilderに対応していないapiDoc-Paramを追加していきライブラリとして成熟させていきたい。

コード

今回紹介したコードはgithubに置いていますので良ければ参照してください。

テストコードはこちらです。