久しぶりに PlayFramework の Controller を実装するなどしてリクエストボディを Form を使ってパースするなどした。CustomMapping を作るパターンを2つ紹介する。
その1. Formatter を実装する
ブログでよく見かけるやつ。
リクエストボディが下記のようになっていたとする。
{ "name": "something string" }
これを下記にマッピングしたい。
case class Whoami(name: Name)
case class Name(value: String)
play.api.data.format.Formatter
の型パラメータに
対象の ValueObject
の型を指定し bind
と unbind
を実装して、
暗黙の関数として定義する。
trait CustomFormatter {
implicit def userFormat: Formatter[Name] = new Formatter[Name] {
override def bind(key: String, data: Map[String, String]): Either[Seq[FormError], Name] = {
Formats.stringFormat.bind(key, data).right.map(Name)
}
override def unbind(key: String, value: Name): Map[String, String] =
Map(key -> value.value)
}
}
使うときは Forms.of
に型パラメータを与えることでよしなにやってくれる。
今回は↑で作った trait
を継承して使う。
object SampleMapper extends CustomFormatter {
val sampleForm = Form(
mapping(
"name" -> Forms.of[Name]
)(Whoami.apply)(Whoami.unapply))
}
作成した Form
を Controller
で使う。
SampleMapper
を import
して bindFromRequest()
を呼び出す。
import com.google.inject.Inject
import javax.inject.Singleton
import play.api.data.Forms.mapping
import play.api.data.format.{Formats, Formatter}
import play.api.data.{Form, FormError, Forms}
import play.api.mvc.{AbstractController, ControllerComponents}
@Singleton
class FormSample @Inject()(cc: ControllerComponents) extends AbstractController(cc) {
import SampleMapper._
def post() = Action { implicit request =>
sampleForm.bindFromRequest().fold(
e => BadRequest(e.errors.toString), whoami => Ok(whoami.toString))
}
}
基本的な使い方はこんな感じ。
その2. Form#transform を使う
Formatter の実装と同様に ValueObject
をマッピングするだけならこっちのが楽ちん。先程の Form の定義を下記のようにするだけ。
型の指定は必須。
Form(
mapping(
"name" -> text.transform[Name](Name, _.value)
)(Whoami.apply)(Whoami.unapply)
)
配列を受け取る場合
例えば下記のように、メールアドレスの配列を受け取ったとする。
{
"name": "something string",
"emails": [
"a@qiita.com",
"b@qiita.com",
"c@qiita.com"
]
}
この場合は組み込みのコレクションマッパーのコンストラクタに指定。
val sampleForm = Form(
mapping(
"name" -> text.transform[Name](Name, _.value),
"emails" -> seq(text.transform[Email](Email, _.value))
)(Whoami.apply)(Whoami.unapply))
case class Whoami(name: Name, emails: Seq[Email])
case class Name(value: String)
case class Email(value: String)
更に emails
を ファーストクラスコレクションとする場合は下記のようになる。
val sampleForm = Form(
mapping(
"name" -> text.transform[Name](Name, _.value),
"emails" -> seq(text.transform[Email](Email, _.value))
.transform[Emails](Emails, _.values)
)(Whoami.apply)(Whoami.unapply))
case class Whoami(name: Name, emails: Emails)
case class Name(value: String)
case class Email(value: String)
case class Emails(values: Seq[Email])
所感
CustomMapping は transform
の利用でほぼこと足りた。
Formatter
を実装するほうが嬉しいときってどんなときだろ?