先日のScalaMatsuri 2018でTwilioの方が発表されていた、SwaggerからAkka HTTP用(http4sにも対応している)のコードを生成するコードジェネレータが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を正としてコード生成を行うので二度手間になりませんし、スペックに従った実装であることをツールで担保できる有用なアプローチなのではないかと思います。