はじめに
SwaggerはAPIドキュメントやモック、クライアントなどを様々なプログラム言語で作成できるツールである。API情報は特定のフォーマットに従うYAML/JSONファイルで与えることができる。ScalaやJavaのようなプログラム言語において、APIの情報はコントローラーの実装部分にアノテーションを付与することがあり、Swaggerもアノテーションを解釈してYAML/JSONを出力する機能がある。これを利用したsbtプラグインはいくつか作られているが、それらが筆者の要求する機能を持たなかったので新しいsbtプラグインsbt-swagger-metaを作成することとした。この記事ではSwaggerのAPI情報を記述するYAML/JSONを自作する際に苦労した点などについて述べる。なおこのプラグインは次のリポジトリで公開されている。
既存のsbtプラグインの問題
アノテーションからSwaggerのファイルを作成するプラグインとしては次のようなものがある。
これらの問題は次のようなものがある。
- YAMLファイルの出力ができない
- 筆者はJSONだけではなく、YAMLも出力したいと考えていた。しかしこれらのプラグインはJSONを出力する機構しか持たない。
- sbt1.0とsbt0.13の両方で使いたい
- 筆者の用途ではsbt1.0とsbt0.13の両方で動かしたいという要求があったが、どちらかにしか対応していない。
- 更新が止っている
- 最終更新が古いので、利用することが躊躇われる。
これらの理由により新しいsbtプラグインを作成することした。
sbt-swagger-metaの使い方
たとえば次のようなScalaファイルがあるとする。
import io.swagger.annotations._
import javax.ws.rs._
import scala.annotation.meta.field
@Path("/users") @Api(value = "/users")
@Produces(Array("application/json"))
object UserEndpoints {
@GET @Path("")
@ApiOperation(value = "Get the key with the supplied key ID.")
@Produces(Array("application/json"))
@ApiResponses(Array(
new ApiResponse(code = 200, message =
"Success. Body contains key and creator information.",
response = classOf[Response.User]
),
new ApiResponse(code = 400, message =
"Bad Request. Errors specify: (snip)",
response = classOf[Response.BadRequest]),
new ApiResponse(code = 404, message = "Not Found.",
response = classOf[Response.NotFound])))
def getByEmail(email: String): Response.User = ???
}
sealed trait Response
object Response {
case class User(
@(ApiModelProperty @field)(value = "email address") email: String
) extends Response
case class BadRequest(
@(ApiModelProperty @field)(value = "error message") msg: String
) extends Response
case class NotFound(
@(ApiModelProperty @field)(value = "error message") msg: String
) extends Response
}
このファイルから次のようなYAMLが得られる。
---
swagger: "2.0"
info:
description: ""
version: "2.0"
title: "API docs"
tags:
- name: "users"
paths:
/users:
get:
tags:
- "users"
summary: "Get the key with the supplied key ID."
description: ""
operationId: "getByEmail_1"
produces:
- "application/json"
parameters:
- in: "body"
name: "body"
required: false
schema:
type: "string"
responses:
200:
description: "Success. Body contains key and creator information."
schema:
$ref: "#/definitions/User"
400:
description: "Bad Request. Errors specify: (snip)"
schema:
$ref: "#/definitions/BadRequest"
404:
description: "Not Found."
schema:
$ref: "#/definitions/NotFound"
definitions:
User:
type: "object"
properties:
email:
type: "string"
description: "email address"
BadRequest:
type: "object"
properties:
msg:
type: "string"
description: "error message"
NotFound:
type: "object"
properties:
msg:
type: "string"
description: "error message"
sbt-swagger-metaの作成で苦労したところ
それでは実際にsbt-swagger-metaを作るにあたって苦労したところをいくつか述べる。
アノテーションを変更してからYAML/JSONファイルを再生成しても変化しない
これは当初、次の方法でPluginClassLoader
を得ていた。
val pluginClassLoader = classOf[com.wordnik.swagger.annotations.Api].getClassLoader.asInstanceOf[PluginClassLoader]
ところが、PluginClassLoader
はシングルトンであるためクラスローダーが変化せずアノテーションを変更してもsbtを再起動しない限り生成されるswagger.json
などが変更されない。そこで、@xuwei-kさんのsbt の Task で、メインの任意のメソッドを実行してその結果を取得するを参考に次のような方法でクラスローダーを作成するようにした。
val mainClassLoader = Internal.makeLoader(fullClasspath.map(_.data), classOf[Api].getClassLoader, scalaInstanceInCompile)
val pluginClassLoader = Internal.makePluginClassLoader(mainClassLoader)
pluginClassLoader.add(fullClasspath.files.map(_.toURI.toURL))
なお、Internal
はsbt1.0とsbt0.13でClasspathUtilities
などのパスが変更されているため、その差を吸収するために次のように作成した。
private[sbtswaggermeta] object Internal {
def makeLoader(classpath: Seq[File], parent: ClassLoader, instance: ScalaInstance): ClassLoader
= internal.inc.classpath.ClasspathUtilities.makeLoader(classpath, parent, instance)
def makePluginClassLoader(classLoader: ClassLoader): PluginClassLoader = new PluginClassLoader(classLoader)
}
private[sbtswaggermeta] object Internal {
def makeLoader(classpath: Seq[File], parent: ClassLoader, instance: ScalaInstance): ClassLoader
= sbt.classpath.ClasspathUtilities.makeLoader(classpath, parent, instance)
def makePluginClassLoader(classLoader: ClassLoader): PluginClassLoader = new PluginClassLoader(classLoader)
}
swagger-scala-moduleを利用してもYAMLの生成が変になる
swagger-scala-moduleはJavaで作られたSwaggerにScalaのOption
やEither
といった型のシリアライズ方法を教えるパッケージであるが、これがYAMLを出力するときに機能しなかった。調べると次のようにJSONの場合のみに変更していた。
object SwaggerScalaModelConverter {
Json.mapper().registerModule(new DefaultScalaModule())
}
そこで、やや強引だが次のようにYAMLにも反映させた。
class SwaggerScalaModelConverterWithYaml extends SwaggerScalaModelConverter
object SwaggerScalaModelConverterWithYaml {
def apply: SwaggerScalaModelConverterWithYaml = {
Yaml.mapper().registerModule(DefaultScalaModule)
new SwaggerScalaModelConverterWithYaml()
}
}
exampleでsbtプラグインを利用する
sbt-swagger-metaはプラグイン本体と、example
というフォルダにこのプラグインの利用例が入っている。このexampleでsbt-swagger-metaを利用するため、最初はsbt publishLocal
して利用していたが、@xuwei-kさんの記事sbt plugin作成時のデバック、テスト方法ではsbt publishLocal
するのではない方法があった。ところが、これをやってみても上手く動作しない。しばらくネットの海を調べたところ、何かのプラグインでexample/project/plugins.sbt
に次のような記述をして解決していた。
lazy val root = Project("plugins", file(".")) dependsOn sbtSwaggerMeta
lazy val sbtSwaggerMeta = ClasspathDependency(RootProject(file("..").getAbsoluteFile.toURI), None)
これにより、version.sbt
とexample/plugins.sbt
でバージョンを二重に管理しなくてもよくなったうえ、sbt publishLocal
していたときはexample側のsbtを再起動しなければならないときがしばしばあったが、それもなくなり一石二鳥であった。
Sonatypeへパブリッシュする際のGPG鍵に空のパスフレーズを利用する
Sonatypeへパブリッシュする際には署名が必要であり、sbt-pgpを利用すればこの作業を簡単に行うことができる。ところがsbt-pgpで鍵を作る際には空のパスフレーズで特に何も警告されないが、いざsbt-releaseを利用していざリリースをするときに次のように表示されて空のパスフレーズが入力できない。
Please enter PGP passphrase (or ENTER to abort):
秘密鍵のパスフレーズを渡す手段として、sbt-pgpのドキュメントによれば~/.sbt/pgp.credentials
というファイルを利用する方法が書かれていたが、これに空のパスフレーズを入れたものの上手くいかなかった。このため、筆者は次のようなファイルpgp.sbt
を利用して次のようにした。
pgpPassphrase := Some(Array())
もし秘密鍵にパスフレーズを設定していないことを明かにしたくない場合は、Gitの機能でpgp.sbt
が見えないようにしておくといいだろう1。
まとめ
これを利用してSwaggerのドキュメントをアノテーションから容易に得られるようになった。もうちょっと使ってみて、なにか追加するべき機能があればひきつづき実装していきたい。また、このプラグインをSonatypeへパブリッシュする際には次の文章を参考にした。
ただし、
sbt release
するときにUnstagedなファイルがあると処理が中断されてしまうので、何かよい手を考える必要がある。↩
コメント
コメントを投稿