LoginSignup
16
12

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-08-13

概要

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

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

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

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

環境

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

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

ソースコードはこちら

ゴール

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

手順

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

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

事前設定

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

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の吐き出し口を用意します。

routes
GET     /swagger.json               controllers.ApiHelpController.getResources

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

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

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

controllerの設定

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

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()

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

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はこのような感じにします。

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するといい感じに表示されるはずです。

「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の記述が汚れてしまいますが、まぁ必要なドキュメントなので構わない?)

16
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
12