SwaggerのYAML/JSONをアノテーションから作るsbtプラグインを作った

はじめに

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のOptionEitherといった型のシリアライズ方法を教えるパッケージであるが、これが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.sbtexample/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へパブリッシュする際には次の文章を参考にした。


  1. ただし、sbt releaseするときにUnstagedなファイルがあると処理が中断されてしまうので、何かよい手を考える必要がある。

コメント