読者です 読者をやめる 読者になる 読者になる

PlayframeworkのFormで要素が一つのcase classと仲良くする

要素が一つのcase classがあると困ること

例えば下のようなcase classをやり取りしたい時、JSONではちょっと困りますよね。

case class LongValue(value: Long) {
  require(value % 2 == 0)
}

JSONの方は仕方なくこうして、

{
  "longValue": {
    "value": 20
  }
}

そして Form はこう

  def hoge() = Form(
    "longValue" -> mapping(
      "value" -> longNumber.verifying(_ % 2 == 0)
    )(LongValue.apply)(LongValue.unapply)
  )

これだけならまだ良いけどこれが配列で…とかになるともう面倒ですよね。 それにせっかくcase classで require を指定してるのに verifying でチェックするのもなんか面倒ですよね。

専用の Formatter を用意する

こんな感じのFormatterを用意してやるといい感じにできます。

  def formatter[T](f:String => T) = new Formatter[T] {
    override val format = None

    override def bind(key: String, data: Map[String, String]): Either[Seq[FormError], T] =
      try {
        data.get(key) match {
          case Some(v) => Right(f(v))
          case _ => Left(Seq(FormError(key, "error.required", Nil)))
        }
      } catch {
        case _: Exception => Left(Seq(FormError(key, s"error.${key}", Nil)))
      }

    override def unbind(key: String, value: T): Map[String, String] = Map(key -> value.toString)
  }

  val longValue = of[LongValue](formatter(s => LongValue(s.toLong)))

これを使うとJSONはこう

{
  "longValue": 20
}

Form はこう

  Form(
    "longValue" -> longValue
  )

LongValue 配列だったとしてもこんなので済みます。

  Form(
    "longValue" -> list(longValue)
  )

必要であれば標準の optionalverifying も使えます。

  Form(
    "longValue" -> optional(longValue.verifying(_.value <= 10))
  )

さらに、他のcase classをつかいたくなったときにはこうです。

case class IntValue(value: Int)

val intValue:FieldMapping[IntValue] = of[IntValue](formatter(s => IntValue(s.toInt)))

普通にマッピング書くよりすこしコード量増えてしまいますが、要素が一つのcase classが頻繁に登場するようであれば Form はむしろスッキリするのではないでしょうか