Playでcirceを使う

[circe](https://circe.github.io/circe/)は、CatsやShapelessを使っている強力なJSONライブラリです。この記事ではPlayでcirceを使うための方法を紹介したいと思います。Playでより実践的な形で使うためにcirceを実装だけではなくテストでも利用する方法を紹介します。

全体のサンプルは以下のリポジトリで見ることができます。

https://github.com/ababup1192/play-circe-sample

# circeの導入

## ライブラリの追加

今回、以下のライブラリを使用します。

```scala
libraryDependencies ++= Seq(
   "org.scalatestplus.play" %% "scalatestplus-play" % "4.0.1" % Test,
   "io.circe" %% "circe-core" % "0.10.0",
   "io.circe" %% "circe-generic" % "0.10.0",
   "io.circe" %% "circe-parser" % "0.10.0",
   "io.tabmo" %% "circe-validation-core" % "0.0.6",
   "io.tabmo" %% "circe-validation-extra-rules" % "0.0.6",
   "com.dripower" %% "play-circe" % "2611.0"
 )
```

## 単純なレスポンスを返す

まずは単純なJsonオブジェクトを返す例を紹介します。ポイントはControllerが返す値をparseする`contentAsJson`のcirce版を用意してあげることです。テストなので乱暴にright.getしています。

```test/controllers/HomeControllerSpec.scala
package controllers

import org.scalatest.FunSpec
import org.scalatestplus.play.guice._
import play.api.mvc.{ControllerComponents, Result}
import play.api.test._
import io.circe.Json
import io.circe.parser._
import io.circe.syntax._
import scala.concurrent.{ExecutionContext, ExecutionContextExecutor, Future}

class HomeControllerSpec extends FunSpec with GuiceOneAppPerTest {
   private[this] def createController(): HomeController = {
     implicit val ec: ExecutionContextExecutor = ExecutionContext.global
     val cc = app.injector.instanceOf[ControllerComponents]
     new HomeController(cc)(ec)
   }

   import play.api.test.Helpers._

   private[this] def contentAsCirceJson(of: Future[Result]): Json =
     parse(contentAsString(of)).right.get

 
   describe("#hello") {
     it("hello world JSONを返す") {
       val controller = createController()

       val result = controller.hello
         .apply(FakeRequest())
       val responseBody = contentAsCirceJson(result)

       assert(status(result) == 200)
       assert(responseBody == Json.obj("hello" -> "world".asJson))
     }
  }
}
```

一旦は空のレスポンスを返すようにしています。

```app/controllers/HomeController.scala
package controllers

import javax.inject._
import play.api.mvc._
import scala.concurrent.{ExecutionContext, Future}

@Singleton
class HomeController @Inject()(
   cc: ControllerComponents
)(implicit ec: ExecutionContext) extends AbstractController(cc) {

  def hello: Action[AnyContent] = Action.async { implicit request =>
    Future(Ok(""))
  }
}
```

Jsonを返していないのでparseに失敗して無事テストがコケます。

<img width="922" alt="スクリーンショット 2019-05-02 12.16.13.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/48687/b583569f-5962-4a55-3a3e-e82e47a24d6b.png">

テストを通すためには、Circe traitをミックスインしJsonを返すように変更を加えます。

```diff
package controllers

import play.api.mvc._
import javax.inject._
import scala.concurrent.{ExecutionContext, Future}
+ import play.api.libs.circe.Circe
+ import io.circe.Json
+ import io.circe.syntax._


@Singleton
class HomeController @Inject()(
  cc: ControllerComponents
+ )(implicit ec: ExecutionContext) extends AbstractController(cc) with Circe {

  def hello: Action[AnyContent] = Action.async { implicit request =>
    Future(
+      Ok(Json.obj("hello" -> "world".asJson))
    )
  }
}
```

無事テストが通りました。

<img width="922" alt="スクリーンショット 2019-05-02 12.21.17.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/48687/e8a3ee5e-fb21-522c-e1d9-17c7b3c9f3c7.png">


## case classを利用してレスポンスを返す。

実際にはcase class等を利用してモデルをレスポンスのJsonオブジェクトに変換するケースが多いでしょう。仮に以下のようなレスポンスを期待します。

```app/controllers/HomeController.scala
describe("#user") {
  it("User JSONを返す") {
    val controller = createController()

    val result = controller.user
      .apply(FakeRequest())
    val responseBody = contentAsCirceJson(result)

    assert(status(result) == 200)
    assert(responseBody ==
      Json.obj(
        "id" -> 1.asJson,
        "name" -> "John".asJson
      )
    )
  }
}
```

先程のhelloメソッドをコピーします。

```app/controllers/HomeController.scala
def user: Action[AnyContent] = Action.async { implicit request =>
  Future(
    Ok(Json.obj("hello" -> "world".asJson))
  )
}
```

差分がわかりやすい形で表示されるので嬉しいですね。

<img width="922" alt="スクリーンショット 2019-05-02 12.26.18.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/48687/815586ee-f771-4bc6-0b38-7224e86691be.png">

大抵の型であれば、以下のimport文を書けば、そのままJsonを書き出すことができます。
`import io.circe.generic.auto._`
CatsやShapelessの威力が発揮される部分です。

```diff
package controllers

import javax.inject._
import scala.concurrent.{ExecutionContext, Future}
import play.api.libs.circe.Circe
import play.api.mvc._
import io.circe.Json
import io.circe.syntax._
+ import io.circe.generic.auto._

+ case class User(id: Int, name: String)

@Singleton
class HomeController @Inject()(
  cc: ControllerComponents
)(implicit ec: ExecutionContext) extends AbstractController(cc) with Circe {

  ...

  def user: Action[AnyContent] = Action.async { implicit request =>
    Future(
+      Ok(User(1, "John").asJson)
    )
  }
}
```

<img width="922" alt="スクリーンショット 2019-05-02 12.31.45.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/48687/ec04ef11-0022-9233-7830-7bff6b02ba09.png">

## リクエストとしてJsonを受け取る

コントローラのテストでリクエストとしてJsonを渡す場合は準備がそれなりに必要です。今まで使用してきた`contentAsCirceJson`と共にHelperとしてtraitに切り出しています。特に重要なのがテストのcallメソッドがJsonを内部でどう受け取り解釈するか、`Writeable[Json]`を必要とします。このロジックはplay-circeが実際に行っている部分なので、ライブラリの中身から必要な部分だけを切り出しています。難しい処理では無いのでサッと読んで理解しておきましょう。また、FakeRequestにcirceが扱いやすいように`withCirceJsonBody`のメソッドをimplicitで生やします。ここまで準備が整ったら、あとは普段のコントローラのテストと変わりありません。

```test/controllers/HomeControllerSpec.scala
package controllers

import akka.stream.Materializer
import org.scalatest.FunSpec
import org.scalatestplus.play.guice._
import play.api.mvc.{ControllerComponents, Result}
import play.api.http._
import play.api.mvc.{Codec, Result}
import play.api.test.FakeRequest
import io.circe._
import io.circe.parser._
import io.circe.syntax._
import scala.concurrent.{ExecutionContext, ExecutionContextExecutor, Future}

trait CirceTestHelper {

  import play.api.test.Helpers._

  def contentAsCirceJson(of: Future[Result]): Json =
    parse(contentAsString(of)).right.get

  implicit class RichFakeRequest[T](fakeRequest: FakeRequest[T]) {
    def withCirceJsonBody(json: Json): FakeRequest[Json] = fakeRequest.withBody[Json](json)
  }

  /**
    * from play.api.libs.circe.Circe
    */
  private val defaultPrinter = Printer.noSpaces

  implicit val contentTypeOf_Json: ContentTypeOf[Json] = {
    ContentTypeOf(Some(ContentTypes.JSON))
  }

  implicit def writableOf_Json(implicit codec: Codec, printer: Printer = defaultPrinter): Writeable[Json] = {
    Writeable(a => codec.encode(a.pretty(printer)))
  }
}

class HomeControllerSpec extends FunSpec with GuiceOneAppPerTest with CirceTestHelper {

  import play.api.test.Helpers._

  private[this] def createController(): HomeController = {
    implicit val ec: ExecutionContextExecutor = ExecutionContext.global
    val cc = app.injector.instanceOf[ControllerComponents]
    new HomeController(cc)(ec)
  }

  ...

  describe("#addUser") {
    it("CREATED Statusを返す") {
      implicit lazy val materializer: Materializer = app.materializer
      val controller = createController()

      val request = FakeRequest("POST", "/user")
        .withCirceJsonBody(
          Json.obj("name" -> "Mike".asJson)
        )
      val result = call(controller.addUser(), request)

      assert(status(result) == CREATED)
    }
  }
}
```

実装上の注意点として、circe.jsonの引数にJsonのサイズを明示的に指定しなければテスト時に値が無いと怒られてしまうので指定をしておきましょう。(テストを書かない場合はデフォルト値がconfから呼び出されます。)

```diff
+ case class UserCommand(name: String)

+   def addUser(): Action[Json] = Action(circe.json(1024)).async { implicit request => 
+     Future(Ok(""))
+ } 
```

無事失敗します。

<img width="922" alt="スクリーンショット 2019-05-02 12.56.29.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/48687/594c8037-9c36-3f9d-248b-e753e1c0a98d.png">

```scala
def addUser(): Action[Json] = Action(circe.json(1024)).async { implicit request =>
  Future(Created)
}
```

<img width="922" alt="スクリーンショット 2019-05-02 12.58.11.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/48687/a6131efa-9a55-94dd-b63a-5cbbdc96db3b.png">

さすがにこれでは味気が無いのでテストケースを追加します。

```scala
  describe("#addUser") {
    describe("nameを渡すと") {
      it("CREATED Statusを返す") {
        implicit lazy val materializer: Materializer = app.materializer
        val controller = createController()

        val request = FakeRequest("POST", "/user")
          .withCirceJsonBody(
            Json.obj("name" -> "Mike".asJson)
          )
        val result = call(controller.addUser(), request)

        assert(status(result) == CREATED)
      }
    }

    describe("100文字より大きいnameを渡すと") {
      it("BAD_REQUEST Statusを返す") {
        implicit lazy val materializer: Materializer = app.materializer
        val controller = createController()

        val request = FakeRequest("POST", "/user")
          .withCirceJsonBody(
            Json.obj("name" -> ("a" * 101).asJson)
          )
        val result = call(controller.addUser(), request)

        assert(status(result) == BAD_REQUEST)
      }
    }
  }
```

<img width="922" alt="スクリーンショット 2019-05-02 13.00.40.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/48687/33faae20-4ddf-1a84-ed67-9bc13d426d9b.png">

バリデーションを含んだDcoderを定義するのは非常に簡単です。自身で[バリデーションルール](https://github.com/tabmo/circe-validation/blob/master/extra-rules/src/main/scala/io.tabmo.circe.extra.rules/StringRules.scala#L24)を定義することも簡単に行なえます。

```scala
import io.circe.{Decoder, Json}
import io.tabmo.circe.extra.rules.StringRules

object UserCommand {
   import io.tabmo.json.rules._

    implicit val decoder: Decoder[UserCommand] = Decoder.instance[UserCommand] { c =>
     for {
       name <- c.downField("name").read(StringRules.maxLength(100))
     } yield UserCommand(name)
   }
 }

 def addUser(): Action[Json] = Action(circe.json(1024)).async { implicit request =>
    Future(
      UserCommand.decoder.decodeJson(request.body) match {
        case Right(_) => Created
        case Left(_) => BadRequest("validation error")
      }
    )
  }
```

<img width="922" alt="スクリーンショット 2019-05-02 13.07.56.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/48687/58d7c696-5d9e-52c9-2fed-983452a97bb1.png">

おまけに空白も許可しないようにしてみましょう。

```scala
describe("空白のnameを渡すと") {
  it("BAD_REQUEST Statusを返す") {
    implicit lazy val materializer: Materializer = app.materializer
    val controller = createController()

    val request = FakeRequest("POST", "/user")
      .withCirceJsonBody(
        Json.obj("name" -> " ".asJson)
      )
    val result = call(controller.addUser(), request)

    assert(status(result) == BAD_REQUEST)
  }
}
```

```diff
+ name <- c.downField("name").read(StringRules.maxLength(100) |+| StringRules.notBlank())
```

# まとめ

circeライブラリ導入の手法を紹介しました。ハマりどころさえ分かっていれば、小さなHelper traitを一つ作るだけで簡単にテストと実装でcirceを使えるようになります。Play Jsonも強力ですが、circeは更に直感的に使え強力です。導入は難しくないので検討してみてはいかがでしょうか？