Akka+AkkaHttp+SlickでTODOアプリAPI（その4）

# 始めに
[前回](https://qiita.com/Mamoru-Izuka/items/035410231b3f24a994c2)に引き続き、今回は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変換をする必要があるのでシリアライザーを定義します。詳しくは[ドキュメント](https://doc.akka.io/docs/akka-http/current/common/json-support.html)を参照してください。

```scala: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を定義しています。

```scala: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](https://doc.akka.io/docs/akka-http/current/routing-dsl/HttpApp.html#httpapp-bootstrap)を使ってみたいのでサーバーを定義します。

```scala: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化を行います。