Edited at

Akka+AkkaHttp+SlickでTODOアプリAPI(その4)

More than 1 year has passed since last update.


始めに

前回に引き続き、今回はAPIルート部分を作成します。


構成


  • IntelliJ 2018

  • Scala 2.12.5

  • sbt 1.1.4

  • Akka 2.5.11

  • AkkaHttp 10.1.0

  • Slick 3.2.3

  • Scalaz 7.2.19


成果物

https://github.com/lightstaff/scala-todo-api


APIルート

まずはTodoActorのコマンドをJson変換をする必要があるのでシリアライザーを定義します。詳しくはドキュメントを参照してください。


TodoActorJsonSerializer.scala

package todo.api.actor

import spray.json._

trait TodoActorJsonSerializer extends DefaultJsonProtocol {

import TodoActor._

implicit val findByIdCommand: RootJsonFormat[FindByIdCommand] = jsonFormat1(FindByIdCommand)
implicit val createCommand: RootJsonFormat[CreateCommand] = jsonFormat1(CreateCommand)
implicit val updateCommand: RootJsonFormat[UpdateCommand] = jsonFormat2(UpdateCommand)
implicit val deleteCommand: RootJsonFormat[DeleteCommand] = jsonFormat1(DeleteCommand)

implicit val todoReplyFormat: RootJsonFormat[TodoReply] = jsonFormat2(TodoReply)
implicit val createdReply: RootJsonFormat[CreatedReply] = jsonFormat1(CreatedReply)

}


続いてルートを定義します。

せっかくなのでFindByIdの結果がNoneだったらNotFoundを返すようにします。

また、例外を捕捉してエラーを返すようにExceptionHandlerを定義しています。


TodoRoute.scala

package todo.api

import scala.util.control.NonFatal

import akka.actor.ActorRef
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.pattern.ask
import akka.util.Timeout

import todo.api.actor.TodoActorJsonSerializer

object TodoRoutes {

def apply(todoActor: ActorRef)(implicit timeout: Timeout): TodoRoutes = new TodoRoutes(todoActor)

}

// TodoActorをインジェクション
class TodoRoutes(todoActor: ActorRef)(implicit timeout: Timeout)
extends SprayJsonSupport
with TodoActorJsonSerializer {

import todo.api.actor.TodoActor._

implicit def todoExceptionHandler: ExceptionHandler = ExceptionHandler {
case NonFatal(ex) =>
extractLog { implicit log =>
extractUri { uri =>
log.error(s"raised error!! uri: $uri, reason: ${ex.getMessage}")
complete(
StatusCodes.InternalServerError -> s"raised error!! uri: $uri, reason: ${ex.getMessage}")
}
}
}

val routes: Route = handleExceptions(todoExceptionHandler) {
extractLog { implicit log =>
extractUri { uri =>
extractMethod { method =>
log.info(s"call api. method: ${method.value}, uri: $uri")
pathPrefix("todos") {
pathEndOrSingleSlash {
get {
onSuccess((todoActor ? FindAllCommand).mapTo[Seq[TodoReply]]) { res =>
complete(StatusCodes.OK -> res)
}
} ~
post {
entity(as[CreateCommand]) { req =>
onSuccess((todoActor ? req).mapTo[CreatedReply]) { res =>
complete(StatusCodes.OK -> res)
}
}
}
} ~
path(IntNumber) { id =>
get {
onSuccess((todoActor ? FindByIdCommand(id)).mapTo[Option[TodoReply]]) {
case Some(todoReply) =>
complete(StatusCodes.OK -> todoReply)
case None =>
complete(StatusCodes.NotFound)
}
} ~
put {
entity(as[UpdateCommand]) { req =>
onSuccess(todoActor ? req) { _ =>
complete(StatusCodes.NoContent)
}
}
} ~
delete {
onSuccess(todoActor ? DeleteCommand(id)) { _ =>
complete(StatusCodes.NoContent)
}
}
}
}
}
}
}
}
}


加えて今回はHttpAppを使ってみたいのでサーバーを定義します。


TodoAPIServer.scala

package todo.api

import scala.concurrent.Await
import scala.concurrent.duration._
import scala.util.Try

import akka.Done
import akka.actor.ActorSystem
import akka.http.scaladsl.server.{HttpApp, Route}

object TodoAPIServer {

def apply(todoRoutes: TodoRoutes): TodoAPIServer = new TodoAPIServer(todoRoutes)

}

// TodoRoutesをインジェクション
class TodoAPIServer(todoRoutes: TodoRoutes) extends HttpApp {

override protected val routes: Route = todoRoutes.routes

override protected def postServerShutdown(attempt: Try[Done], system: ActorSystem): Unit = {
super.postServerShutdown(attempt, system)

system.terminate()
Await.result(system.whenTerminated, 30.seconds)
()
}
}


以上でActorのコマンドの扱いがなんだか微妙ですが、APIルートは完成です。


終わりに

次回は仕上げにエントリーポイントを定義し、DockerImage化を行います。