これはPlay framework Advent Calendar 2014の24日目です
アドベントカレンダー空いてたのと、たまたま思い出したので、また 前回書いたものと関係あるような play-jsonのReadsやWritesの話をします。
といっても、前回にくらべれば短いです。*1だいたい以下のコードで言いつくされている。
そういえばEitherのReadsやWritesって、現状play内部には存在してないし、たまに作って使ったことあるけど、blogとかに貼ったことなかった気がするので。この場合のEitherは、成功か失敗かという使い方ではなく、単に2つのうちのどちらかを表現してるという使い方です。
ググっても、一発でわかりやすいのが出ないようですね?
playのMLに質問きてたので、gistのURLをはりつけるだけの回答をしておきました
https://gist.github.com/xuwei-k/e439479b3b4a6ac04322
https://groups.google.com/d/topic/play-framework/anYvYB-8GYs/discussion
上記では、Readsが失敗した場合2つのエラーを単にmergeしてますが、そうではなく「2つそれぞれを試したけど、両方失敗しました」という感じのメッセージを自作して埋め込んだほうが親切かもしれません?そのあたりは各自で工夫してください。
あと、上記のReadsの実装は「片方を試して成功したら、もう片方は試さないで最初の成功を返す」ですが「Readsが2つとも成功してしまったら、逆にエラーにする」という挙動が欲しい場合もあり得るかもしれません。*2その場合は以下
def eitherReads[A, B](implicit A: Reads[A], B: Reads[B]): Reads[Either[A, B]] = Reads{ json => (A.reads(json), B.reads(json)) match { case (JsSuccess(l, _), _: JsError) => JsSuccess(Left(l)) case (_: JsError, JsSuccess(r, _)) => JsSuccess(Right(r)) case (JsSuccess(_, _), JsSuccess(_, _)) => JsError("両方成功しちゃったのでエラーにしたよ(・ω<)") // ここのメッセージはご自由に。引数でとるようにしてもいいかも case (e1: JsError, e2: JsError) => e1 ++ e2 } }
FormatやOFormatは作ってないけれど、play側にimplicitがあるので、直接定義しなくてもいけるはずです。
また、「Scala標準のEitherは使いづらいので使いたくない」とか「コンパニオンオブジェクトにimplicitで定義したい(そのほうがデフォルトでスコープに入って便利)」という需要があれば、いっそのことEitherと同型のclassを自作してもいいかもしれません*3
「毎回ReadsとWritesからFormatのインスタンスが生成されるのが微妙に無駄でいやだ!」
とか
「(整合性保ちやすいように)そもそもReadsやWrites片方だけでなく、必ず両方を定義する決まりにしてるんだ!」
などのこだわりがあれば、逆にReadsとWritesをimplicitで定義するのではなく、FormatやOFormatだけを定義したほうがいいかもしれません。
もし「インスタンスの生成コスト」まで気になるなら、以下のように「implicitを使わずにFormatを定義」しておいて、明示的に呼び出して結果をvalで束縛とかしましょう。
// implicit つけずに定義 def eitherFormat[A, B](implicit A: Format[A], B: Format[B]): Format[Either[A, B]] = case class Foo( /* 定義は略 */ ) object Foo { implicit val format: Format[Foo] = // 略 } case class Bar( /* 定義は略 */ ) object Bar { implicit val format: Format[Bar] = // 略 } // 明示的に呼び出してvalで束縛すれば、毎回生成されずに済む implicit val fooOrBar: Format[Either[Foo, Bar]] = eitherFormat[Foo, Bar]