swagger-playでAPIドキュメントを自動生成する


## 概要

フロントエンジニアや外部に公開するAPIを用意する時、APIドキュメントを書くのは地味に面倒ですよね。

最近ではswaggerを筆頭に、ドキュメント生成ツール（Mockサーバーも内蔵しているものもある）が充実してきているので、それを使っていい感じにドキュメントを作りたいと思います。

play2もswagger pluginがあるので、それを使ってドキュメント生成をコード側でやってしまいましょう。

（筆者はswaggerを使ったことがなかったので、swagger自体の使い方も含んだ記事になります。）

## 環境

（執筆時点では、swagger-playがplay2.5対応しきれていなかったので、forkされたものを使用しています）

- Play 2.5
- swagger-play (CreditCardsComがforkしたもの)

ソースコードはこちら

https://github.com/uryyyyyyy/play2sample/tree/play2-swagger

## ゴール

- （アノテーションベースで）ある程度わかりやすく書けることを確認する
- 本番の動作に影響しないことを確認する
- 正しくドキュメントが作られていることを確認する
- モックサーバとしても使えることを確認する

## 手順

- controllerにアノテーションを付けてswagger.jsonを吐く
- 生成したjsonをswagger-editorに入れて、swagger-uiを生成する
- swagger-uiでドキュメントが正しく表示されていることを確認する
- swagger-uiをモックサーバとして使えることを確認する

### controllerにアノテーションを付けてswagger.jsonを吐く

#### 事前設定

まずはswagger-playを組み込みます。swagger公式のものがまだ対応していなかったのでforkされたものを対象のplayプロジェクトに依存させます。

```scala:build.scala
lazy val webConsole = (project in file("webConsole"))
  .enablePlugins(PlayScala)
  .dependsOn(swagger)
  .settings(commonSettings)
  .settings(Seq(
    name := "play2Sample-main"
    )
  ))

lazy val swagger = RootProject(uri("ssh://git@github.com/CreditCardsCom/swagger-play.git"))
```

生成したswagger.jsonの吐き出し口を用意します。

```text:routes
GET     /swagger.json               controllers.ApiHelpController.getResources
```

confに以下の設定を組み込みます。
これで、上記APIにアクセスした際にリフレクションを駆使してjsonを生成してくれます。
この設定を外しても他のAPIは問題なく動くので、本番時はこの設定を外せば問題ないと思います。

```application.conf
play.modules.enabled += "play.modules.swagger.SwaggerModule"
```

これで、swaggerを使う準備が整いました。

#### controllerの設定

次にcontrollerを作っていきます。
routesに以下の設定を追加します。

```text:routes
...
GET     /swagger/getById/:id                   controllers.swagger.UserController.getById(id: Long)
GET     /swagger/getAll                   controllers.swagger.UserController.getAll()
POST     /swagger/edit                   controllers.swagger.UserController.edit()
```

今回使うモデルも定義します。

```scala:DTOs.scala
package controllers.swagger
import play.api.libs.json.Json

case class UserDTO(id: Long, name: String)

object UserDTO {
  implicit val writes = Json.writes[UserDTO]
  implicit val reads = Json.reads[UserDTO]
}

case class UserWithTimeStampDTO(user: UserDTO, unixTime: Long)

object UserWithTimeStampDTO {
  implicit val writes = Json.writes[UserWithTimeStampDTO]
  implicit val reads = Json.reads[UserWithTimeStampDTO]
}

case class MessageDTO(id: String, messages: List[String])

object MessageDTO {
  implicit val writes = Json.writes[MessageDTO]
  implicit val reads = Json.reads[MessageDTO]
}
```

Controllerはこのような感じにします。

```scala:UserController.scala
package controllers.swagger

import javax.inject.{Inject, Singleton}

import io.swagger.annotations._
import play.api.libs.json.{JsError, Json}
import play.api.mvc.{Action, BodyParsers, Controller}

@Singleton
@Api(value = "userAPI")
class UserController @Inject() () extends Controller {

  @ApiOperation(
    produces = "application/json",
    consumes = "application/json",
    httpMethod = "GET",
    value = "fetch user by id",
    response = classOf[UserDTO]
  )
  @ApiResponses(Array(
    new ApiResponse(code = 400, message = "Invalid ID", response = classOf[MessageDTO]),
    new ApiResponse(code = 404, message = "target user not found", response = classOf[MessageDTO]))
  )
  def getById(
    @ApiParam(value = "id used by fetch target user") id: Long
  ) = Action {
    val user = UserDTO(id, "uryyyyyyy")
    Ok(Json.toJson(user))
  }

  @ApiOperation(
    produces = "application/json",
    consumes = "application/json",
    httpMethod = "GET",
    value = "fetch all users",
    response = classOf[Array[UserDTO]]
  )
  def getAll() = Action {
    val users = Array(UserDTO(1, "uryyyyyyy"), UserDTO(2, "uryyyyyyy2"))
    Ok(Json.toJson(users))
  }

  @ApiOperation(
    produces = "application/json",
    consumes = "application/json",
    httpMethod = "POST",
    value = "save user",
    response = classOf[MessageDTO]
  )
  @ApiImplicitParams(Array(
    new ApiImplicitParam(name = "user", value = "User with timestamp", required = true, dataType = "controllers.swagger.UserWithTimeStampDTO", paramType = "body")
  ))
  @ApiResponses(Array(
    new ApiResponse(code = 400, message = "bad query", response = classOf[MessageDTO])
  ))
  def edit() = Action(BodyParsers.parse.json) { request =>
    request.body.validate[UserWithTimeStampDTO].asEither match {
      case Left(errors) => BadRequest(Json.obj("status" ->"BAD", "message" -> JsError.toJson(errors)))
      case Right(user) => {
        //save method
        val msg = MessageDTO("id1", List(s"user '${user.user.id}' saved."))
        Ok(Json.toJson(msg))
      }
    }
  }

}
```

少々長いですが、`io.swagger.annotations`のJavaDocを読めば書き方はすぐにわかるので割愛します。

気をつけるポイントとしては、Optionやjava.timeなど少し複雑なクラスを使うと、swaggerが判断しきれずにstringとみなされる。

responseフィールドでは型の参照をそのまま使えるが、なぜかApiImplicitParamではそれができなく、dataTypeで完全修飾名を付ける必要がある。こちらも上手く参照できないとstringになる。

ここまで用意して、`http://localhost:9000/swagger.json`にアクセスすると、生成されたjsonが吐き出されます。（長いので割愛）

### 生成したjsonをswagger-editorに入れて、swagger-uiを生成する

無精なのでオンラインのものを使います。
下記へアクセスして、「File」で先ほど生成したjsonをimportするといい感じに表示されるはずです。

http://editor.swagger.io/#/

「generate server」を押すとswagger-ui（兼 モックサーバ）を生成してくれるので、適当なプラットフォームで試します。
僕はNodeを使いましたが、package.jsonのversionが「"beta"はInvalidだ」などと言われたので記述を消して実行しました。

### swagger-uiでドキュメントが正しく表示されていることを確認する

無事サーバが立ち上がったら、

`http://localhost:9000/docs`

に繋いで、自分が設定したAPIがちゃんと見えてることを確認します。

ちなみにこのとき、画面上部のテキスト入力欄に違うswagger.jsonのエンドポイントを指定すると、それに応じてドキュメントも変わってくれます。

### swagger-uiをモックサーバとして使えることを確認する

swagger-uiから試しにAPIリクエストを投げてみると、期待通りのレスポンスが返ってくるはずです。
ここで、生成されたcurlコマンドなどを投げても、ちゃんとjsonが返ってくることが確認出来ると思います。


## まとめ

認証とか絡むとまた面倒かもしれませんが、手軽に作れていいのではないでしょうか。
（controllerの記述が汚れてしまいますが、まぁ必要なドキュメントなので構わない？）