Scala
swagger
Akka-HTTP

akka-httpにswagger2.xを組み込む方法

akka-httpswagger2.xを組み込む方法を以下に示します。

※DIコンテナであるAirframeを使っていますが、もちろん必須ではありません。適宜読み替えてくだだい。


ライブラリの依存関係

swagger-akka-httpを追加します。javax.ws.rs-apiはアノテーションを利用するために追加します。akka-http-corsは必要に応じて追加してください。

libraryDependencies ++= Seq(

"com.typesafe.akka" %% "akka-http" % "10.1.5",
"com.github.swagger-akka-http" %% "swagger-akka-http" % "2.0.0"
"javax.ws.rs" % "javax.ws.rs-api" % "2.0.1"
"ch.megard" %% "akka-http-cors" % "0.3.0"
// ...
)


コントローラの例

コントローラに相当するクラスにアクションを作って、そのメソッドにアノテーションを割り当てます。別にコントローラを定義しなくとも、エンドポイントごとにRoute定義が分かれていて、アノテーションが付与できればよいです。

アノテーションの使い方は、Swagger 2.X Annotationsを読んでください。

package spetstore.interface.api.controller

import java.time.ZonedDateTime

import akka.http.scaladsl.server.{Directives, Route}
import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport._
import io.circe.generic.auto._
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._
import monix.eval.Task
import monix.execution.Scheduler
import org.hashids.Hashids
import org.sisioh.baseunits.scala.money.Money
import spetstore.domain.model.basic.StatusType
import spetstore.domain.model.item._
import spetstore.interface.api.model.{CreateItemRequest, CreateItemResponse, CreateItemResponseBody}
import spetstore.interface.generator.jdbc.ItemIdGeneratorOnJDBC
import spetstore.interface.repository.ItemRepository
import wvlet.airframe._

import scala.concurrent.Future

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

private val itemRepository: ItemRepository[Task] = bind[ItemRepository[Task]]

private val itemIdGeneratorOnJDBC: ItemIdGeneratorOnJDBC = bind[ItemIdGeneratorOnJDBC]

private val hashids = bind[Hashids]

def route: Route = create

private def convertToAggregate(id: ItemId, request: CreateItemRequest): Item = Item(
id = id,
status = StatusType.Active,
name = ItemName(request.name),
description = request.description.map(ItemDescription),
categories = Categories(request.categories),
price = Price(Money.yens(request.price)),
createdAt = ZonedDateTime.now(),
updatedAt = None
)

@POST
@Operation(
summary = "Create item",
description = "Create Item",
requestBody =
new RequestBody(content = Array(new Content(schema = new Schema(implementation = classOf[CreateItemRequest])))),
responses = Array(
new ApiResponse(responseCode = "200",
description = "Create response",
content = Array(new Content(schema = new Schema(implementation = classOf[CreateItemResponse])))),
new ApiResponse(responseCode = "500", description = "Internal server error")
)
)
def create: Route = path("items") {
post {
extractActorSystem { implicit system =>
implicit val scheduler: Scheduler = Scheduler(system.dispatcher)
entity(as[CreateItemRequest]) { request =>
val future: Future[CreateItemResponse] = (for {
itemId <- itemIdGeneratorOnJDBC.generateId()
_ <- itemRepository.store(convertToAggregate(itemId, request))
} yield CreateItemResponse(Right(CreateItemResponseBody(hashids.encode(itemId.value))))).runAsync
onSuccess(future) { result =>
complete(result)
}
}
}
}
}

// ...

}


swagger-ui

swagger-uidistsrc/main/resource/swaggerとしてコピーしてください。


SwaggerHttpService

次にSwaggerHttpServiceの実装を用意します。

package spetstore.interface.api

import com.github.swagger.akka.SwaggerHttpService
import com.github.swagger.akka.model.Info

class SwaggerDocService(hostName: String, port: Int, val apiClasses: Set[Class[_]]) extends SwaggerHttpService {
override val host = s"127.0.0.1:$port" //the url of your api, not swagger's json endpoint
override val apiDocsPath = "api-docs" //where you want the swagger-json endpoint exposed
override val info = Info() //provides license and other description details
override val unwantedDefinitions = Seq("Function1", "Function1RequestContextFutureRouteResult")
}


routeの設定

akka-httpのRouteは以下を参考にしてください。SwaggerDocServiceとコントローラをrouteに加えます。また、CORSが必要なら、"ch.megard" %% "akka-http-cors" % "0.3.0" を使うとよいと思います。

package spetstore.interface.api

import akka.http.scaladsl.model.{ ContentTypes, HttpEntity, HttpResponse }
import akka.http.scaladsl.server.{ Directives, Route, StandardRoute }
import wvlet.airframe._
import ch.megard.akka.http.cors.scaladsl.CorsDirectives._
import spetstore.interface.api.controller.ItemController

trait Routes extends Directives {

private lazy val itemController = bind[ItemController]
private lazy val swaggerDocService = bind[SwaggerDocService]

private def index(): StandardRoute = complete(
HttpResponse(
entity = HttpEntity(
ContentTypes.`text/plain(UTF-8)`,
"Wellcome to API"
)
)
)

def routes: Route = cors() {
pathEndOrSingleSlash {
index()
} ~ path("swagger") {
getFromResource("swagger/index.html")
} ~ getFromResourceDirectory("swagger") ~
swaggerDocService.routes ~ itemController.route
}

}


ブートストラップ

akka-httpの起動部分のコードです。

package spetstore.interface.api

import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.Http.ServerBinding
import akka.http.scaladsl.settings.ServerSettings
import akka.stream.ActorMaterializer
import wvlet.airframe._

import scala.concurrent.Future
import scala.util.{ Failure, Success }

trait ApiServer {

implicit val system = bind[ActorSystem]
implicit val materializer = ActorMaterializer()
implicit val executionContext = system.dispatcher

private val routes = bind[Routes].routes

def start(host: String, port: Int, settings: ServerSettings): Future[ServerBinding] = {
val bindingFuture = Http().bindAndHandle(handler = routes, interface = host, port = port, settings = settings)
bindingFuture.onComplete {
case Success(binding) =>
system.log.info(s"Server online at http://${binding.localAddress.getHostName}:${binding.localAddress.getPort}/")
case Failure(ex) =>
system.log.error(ex, "occurred error")
}
sys.addShutdownHook {
bindingFuture
.flatMap(_.unbind())
.onComplete { _ =>
materializer.shutdown()
system.terminate()
}
}
bindingFuture
}

}

アプリケーションのブートストラップ部分です。

package spetstore.api

import akka.actor.ActorSystem
import akka.http.scaladsl.settings.ServerSettings
import monix.eval.Task
import org.hashids.Hashids
import slick.basic.DatabaseConfig
import slick.jdbc.JdbcProfile
import spetstore.domain.model.item.ItemId
import spetstore.interface.api.controller.ItemController
import spetstore.interface.api.{ApiServer, Routes, SwaggerDocService}
import spetstore.interface.generator.IdGenerator
import spetstore.interface.generator.jdbc.ItemIdGeneratorOnJDBC
import spetstore.interface.repository.{ItemRepository, ItemRepositoryBySlick}
import wvlet.airframe._

/**
* http://127.0.0.1:8080/swagger
*/

object Main {

def main(args: Array[String]): Unit = {
val parser = new scopt.OptionParser[AppConfig]("spetstore") {
opt[String]('h', "host").action((x, c) => c.copy(host = x)).text("host")
opt[Int]('p', "port").action((x, c) => c.copy(port = x)).text("port")
}
val system = ActorSystem("spetstore")
val dbConfig: DatabaseConfig[JdbcProfile] =
DatabaseConfig.forConfig[JdbcProfile](path = "spetstore.interface.storage.jdbc", system.settings.config)

parser.parse(args, AppConfig()) match {
case Some(config) =>
val design = newDesign
.bind[Hashids].toInstance(new Hashids(system.settings.config.getString("spetstore.interface.hashids.salt")))
.bind[ActorSystem].toInstance(system)
.bind[JdbcProfile].toInstance(dbConfig.profile)
.bind[JdbcProfile#Backend#Database].toInstance(dbConfig.db)
.bind[Routes].toSingleton
.bind[SwaggerDocService].toInstance(
new SwaggerDocService(config.host, config.port, Set(classOf[ItemController]))
)
.bind[ApiServer].toSingleton
.bind[ItemRepository[Task]].to[ItemRepositoryBySlick]
.bind[IdGenerator[ItemId]].to[ItemIdGeneratorOnJDBC]
.bind[ItemController].toSingleton
design.withSession { session =>
val system = session.build[ActorSystem]
session.build[ApiServer].start(config.host, config.port, settings = ServerSettings(system))
}
case None =>
println(parser.usage)
}
}
}


まとめ

最低限、考慮すべきこととしては、akka-httpのroute dslはエンドポイントごとに分割してswaggerアノテーションを割り当てれるようにしてください。これ以外はswaggerの一般的な使い方と変わりません。