Twilio社がOSS化したScala用SwaggerコードジェネレータGuardrailを試してみる

先日のScalaMatsuri 2018でTwilioの方が発表されていた、SwaggerからAkka HTTP用(http4sにも対応している)のコードを生成するコードジェネレータがGitHubで公開されていました。

github.com

発表を聞いていて良さげな感じだったのでちょっと試してみました。

まずはGitHubからリポジトリをクローンしてきます。

$ git clone https://github.com/twilio/guardrail.git
$ cd guardrail

コード生成にはルートディレクトリのcli.shというシェルスクリプトを使います。ソースツリーにサンプルのSwaggerファイルが含まれているのでこれを指定してAkka HTTP用のコードを生成してみます。クライアント側とサーバ側のコードをそれぞれ生成できるようです。

$ ./cli.sh --client --specPath modules/codegen/src/main/resources/petstore.json --packageName petstore --outputPath /tmp/petstore-client/src/main/scala
$ ./cli.sh --server --specPath modules/codegen/src/main/resources/petstore.json --packageName petstore --outputPath /tmp/petstore-server/src/main/scala

このとき--framework http4sというオプションを付けるとhttp4s用のコードを生成することもできるようです。

生成されるのはコードのみなので実行するにはbuild.sbtなどを別途作成する必要があります。それぞれのプロジェクトに以下のような感じのbuild.sbtを作成しました。

name := "petstore-server"
version := "1.0"
scalaVersion := "2.12.5"
scalacOptions := Seq("-unchecked", "-deprecation", "-encoding", "utf8")
libraryDependencies ++= {
  val akkaHttpV   = "10.0.10"
  val catsVersion = "0.9.0"
  val circeVersion = "0.8.0"
  Seq(
    "com.typesafe.akka" %% "akka-http" % akkaHttpV,
    "io.circe" %% "circe-core" % circeVersion,
    "io.circe" %% "circe-generic" % circeVersion,
    "io.circe" %% "circe-java8" % circeVersion,
    "io.circe" %% "circe-parser" % circeVersion,
    "org.typelevel" %% "cats" % catsVersion
  )
}

ではサーバから見ていきましょう。実際に処理を行うハンドラ部分がトレイトとして生成されているのでこれを実装する必要があります。

object Main extends App {
  implicit val system: ActorSystem = ActorSystem("helloAkkaHttpServer")
  implicit val materializer: ActorMaterializer = ActorMaterializer()
  implicit val executionContext: ExecutionContext = system.dispatcher
  
  val userHandler = new UserHandler {
    override def getUserByName(respond: UserResource.getUserByNameResponse.type)(username: String): Future[UserResource.getUserByNameResponse] = {
      Future(respond.OK(User(username = Some("takezoe"))))
    }
    override def createUser(respond: UserResource.createUserResponse.type)(body: User): Future[UserResource.createUserResponse] = {
      Future(respond.OK)
    }
    // TODO: Implement other methods
    ...
  }

  val routes = UserResource.routes(userHandler)
  val serverBindingFuture: Future[ServerBinding] = Http().bindAndHandle(routes, "0.0.0.0", 8081)
  ...
}

ハンドラの各メソッドの最初の引数リストにはレスポンスのためのオブジェクトの型、2つめの引数リストにはSwaggerで定義したパラメータ群が渡されてきます。レスポンスのステータスもSwaggerで定義したものしか返せないようタイプセーフになっているのが嬉しいところです。

続いてクライアントサイドです。こちらはHttpClientを定義する必要があります。これはAkka HTTPのHttpRequestを受け取りFuture[HttpResponse]を返す関数のエイリアスなのですが、トレーシングなどのために処理を挟めるよう関数で抽象化されているようです。

object Main extends App {
  implicit val system: ActorSystem = ActorSystem("helloAkkaHttpClient")
  implicit val materializer: ActorMaterializer = ActorMaterializer()
  implicit val executionContext: ExecutionContext = system.dispatcher
  implicit val httpClient: HttpClient = (request: HttpRequest) => Http().singleRequest(request)

  val client = UserClient("http://localhost:8081")

  val result = for {
    _    <- client.createUser(User(username = Some("takezoe")))
    user <- client.getUserByName("takezoe")
  } yield user

  val user = Await.result(result.value, Duration.Inf)
  println(user)
}

クライアントの各メソッドの戻り値はCatsのEitherTになっているのでfor式で簡単に合成することができます。また、クライアント、サーバ共に生成されたコードを直接編集せずに処理を実装することができるようになっているので、Swaggerファイルを変更した場合は単純にもう一度コードを生成しなおせばよさそうです。

ScalaのWebフレームワークにおけるSwaggerとの連携機能は、コードからSwaggerのAPI定義を生成することを目的としたものが多く、API定義をDSLソースコードに埋め込むものが多いのですが、これだと二度手間ですし、実装と定義の整合性をプログラマが担保しなくてはなりません。GuardrailはSwaggerを正としてコード生成を行うので二度手間になりませんし、スペックに従った実装であることをツールで担保できる有用なアプローチなのではないかと思います。

ちなみに試しに生成してみたAkka HTTPサーバとクライアントのソースコードは以下のリポジトリに置いてあります。

github.com