akka-http
にswagger2.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-uiのdist
をsrc/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の一般的な使い方と変わりません。