概要
フロントエンジニアや外部に公開する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プロジェクトに依存させます。
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の吐き出し口を用意します。
GET /swagger.json controllers.ApiHelpController.getResources
confに以下の設定を組み込みます。
これで、上記APIにアクセスした際にリフレクションを駆使してjsonを生成してくれます。
この設定を外しても他のAPIは問題なく動くので、本番時はこの設定を外せば問題ないと思います。
play.modules.enabled += "play.modules.swagger.SwaggerModule"
これで、swaggerを使う準備が整いました。
controllerの設定
次にcontrollerを作っていきます。
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()
今回使うモデルも定義します。
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はこのような感じにします。
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の記述が汚れてしまいますが、まぁ必要なドキュメントなので構わない?)