Akka-HttpとSwaggerを連携させる

連携させるのにハマったので備忘録です。

# 使用するライブラリ
``` scala
libraryDependencies ++= Seq(
  "com.typesafe.akka"            %% "akka-http"         % "10.1.7",
  "com.github.swagger-akka-http" %% "swagger-akka-http" % "2.0.3",
  "javax.ws.rs"                  % "javax.ws.rs-api"    % "2.0.1",
  "ch.megard"                    %% "akka-http-cors"    % "0.4.1"
)
```

# APIエンドポイントの定義

/itemsでレスポンスを返すAPIを作成します

``` scala:ItemContoller
package json_server.http.controller

import akka.http.scaladsl.model.{ ContentType, HttpEntity, HttpResponse, MediaTypes, StatusCodes }
import akka.http.scaladsl.server.{ Directives, Route }
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.media.{ Content, Schema }
import io.swagger.v3.oas.annotations.parameters.RequestBody
import io.swagger.v3.oas.annotations.responses.ApiResponse
import javax.ws.rs._

@Path("/items")
@Consumes(Array("application/json"))
@Produces(Array("application/json"))
class ItemController extends Directives {

  def route: Route = create

  @GET
  @Operation(
    summary = "Get Items",
    description = "",
    requestBody = new RequestBody(content = Array(new Content(schema = new Schema()))),
    responses = Array(
      new ApiResponse(responseCode = "200",
                      description = "Create response",
                      content = Array(new Content(schema = new Schema()))),
      new ApiResponse(responseCode = "500", description = "Internal server error")
    )
  )
  def create: Route = path("items") {
    get {
      complete(
        HttpResponse(entity = HttpEntity(ContentType(MediaTypes.`application/json`), """
          {"items":"pen"}
          """))
      )
    }
  }
}
```

# SwaggerDocServiceの定義

```scala:SwaggerDocService.scala

package json_server.http

import com.github.swagger.akka.SwaggerHttpService
import com.typesafe.config.ConfigFactory

class SwaggerDocService(val apis: Set[Class[_]]) extends SwaggerHttpService {

  val conf = ConfigFactory.load

  val address = conf.getString(MY_HOST)    // 0.0.0.0
  val port    = conf.getInt(MY_PORT)       // 5000

  override val apiClasses          = apis
  override val host                = s"$address:$port"
  override val apiDocsPath         = "api-docs"         // このパスでyaml/jsonを生成する
  override val schemes             = List("https")      // httpsを使いたい場合はhttpsを指定
}
```

# Routeの定義

``` scala:Routes.scala
package json_server.http

import akka.actor.ActorSystem
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.{ ExceptionHandler, Route }
import akka.util.Timeout
import ch.megard.akka.http.cors.scaladsl.CorsDirectives._ 

import json_server.http.controller.ItemControlle

import scala.util.control.NonFatal

class Routes()(implicit system: ActorSystem, timeout: Timeout) extends SprayJsonSupport {

  /***
    * Custom Error Handler
    */
  implicit def customExceptionHandler: ExceptionHandler = ExceptionHandler {
    case NonFatal(ex) =>
      extractLog { implicit log =>
        extractUri { uri =>
          val errorMessage = s"ERROR!! URI: $uri, REASON: ${ex.getMessage}"

          log.error(errorMessage)

          complete(StatusCodes.InternalServerError -> errorMessage)
        }
      }
  }

  /***
    * Routing
    */
  val routes: Route = (cors() | handleExceptions(customExceptionHandler)) { // Route作成時にcors()を呼ばないとSwaggerでExecuteした時にレスポンスの内容が画面に表示されない
    extractLog { implicit log =>
      extractUri { uri =>
        extractMethod { method =>
          log.info(s"call api. method: ${method.value}, uri: $uri")

          path("swagger") {
            getFromResource("swagger/index.html")
          } ~ 
          // swaggerディレクトリ下のファイルにアクセス可能にする
          getFromResourceDirectory("swagger") ~ 
          // APIエンドポイントとなるクラスをSwaggerHttpService.apiClassesに渡す
          new SwaggerDocService(Set(classOf[ItemController])).routes ~ 
          // エンドポイントのルーティングを追加
          new ItemController().route
        }
      }
    }
  }
}
```

# swagger.yamlを生成する
ブラウザで以下のURLにアクセスする。

``` bash
https://localhost:5000/api-docs/swagger.yaml
```

すると、以下のyaml形式のテキストがブラウザに表示される。

``` yaml
openapi: 3.0.1
info:
  title: ""
  description: ""
  termsOfService: ""
  version: ""
servers:
- url: https://0.0.0.0:5000
security: []
paths:
  /items:
    get:
      summary: Get Items
      operationId: create
      requestBody:
        content:
          application/json: {}
      responses:
        200:
          description: Create response
          content:
            application/json: {}
        500:
          description: Internal server error
components:
  schemas:
    Function1RequestContextFutureRouteResult:
      type: object
```
`resources`下に`swagger.yaml`を作成し、この内容をコピペする。

# Swaggerの実行
ブラウザで以下のURLにアクセスすると、Swagger UIが起動する。

``` bash
https://localhost:5000/swagger
```
ペットストアのAPIサービスらしい。
![ワークスペース 1_003.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64360/255adfc6-eee2-ef23-50f0-1e59b0efff9f.png)

画面上部のテキストボックスに、ペットストア用のSwaggerリソース`https://petstore.swagger.io/v2/swagger.json`が指定されているので、先程作成した`swagger.yaml`を指定する。
実行するとSwaggerでエンドポイントにアクセスすることができるようになる。
![範囲を選択_001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64360/c2289bab-9b31-c35b-7715-61202526b68c.png)

毎回`swagger.yaml`を指定するのが面倒なので、以下のURLで画面を表示すれば`swagger.yaml`を読み込んだ状態でスタートできる。

``` bash
https://localhost:5000/swagger?https://localhost:5000/swagger.yaml
```

参考：
[akka-httpにswagger2.xを組み込む方法](https://qiita.com/j5ik2o/items/75362c7e3bbabe8afcfa)