Created
February 14, 2018 19:17
-
-
Save realpeterz/aecb53a67fb723485eb66d544a67d580 to your computer and use it in GitHub Desktop.
Generic Play Json ADT Format (Writes & Reads)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import play.api.libs.json._ | |
import scala.reflect.ClassTag | |
trait AdtFormat[A <: Product with Serializable] { | |
type ReadByTypeFunc[C <: A] = PartialFunction[String, JsResult[C]] | |
type ReadsFunc[B <: A] = JsValue => JsResult[B] | |
type WriteFunc = PartialFunction[A, JsValue] | |
def singletonReadFunc: ReadsFunc[A] = readError | |
def nonSingletonReadFunc: ReadsFunc[A] = readError | |
def writeFunc: WriteFunc = writeNull | |
val typeFieldName = "_type" | |
val dataFieldName = "_data" | |
private val initialToLower: (String => String) = { | |
case xs: String if xs.length > 0 => xs.head.toLower + xs.tail | |
case _ => "" | |
} | |
private val validationMessages = Seq(s"${typeFieldName} undefined or incorrect") | |
private val error: JsonValidationError = JsonValidationError(validationMessages) | |
protected val writeNull: PartialFunction[A, JsValue] = { case _ => JsNull } | |
protected val readError: ReadsFunc[A] = _ => JsError("Reads not defined") | |
protected val typeFromJsValue: JsValue => JsResult[String] = (JsPath \ typeFieldName).read[String].reads(_).orElse(JsError(error)) | |
protected def readNonSingletonByType[C <: A](typeName: String, formatC: Format[C])(jsValue: JsValue): ReadByTypeFunc[C] = { | |
case `typeName` => (JsPath \ dataFieldName).read(formatC).reads(jsValue) | |
} | |
protected def readNonSingletons(rs: Seq[JsValue => ReadByTypeFunc[A]]): ReadsFunc[A] = (jsValue: JsValue) => { | |
require(!rs.isEmpty, "readNonSingletons only takes a non-empty Seq") | |
val readByTypeFuncs = rs match { | |
case head :: Nil => head(jsValue) | |
case head :: tail => tail.foldLeft(head(jsValue))((acc, f) => acc orElse f(jsValue)) | |
} | |
typeFromJsValue(jsValue).fold(_ => JsError(error), readByTypeFuncs) | |
} | |
protected def writeNonSingleton[C <: A](typeName: String, formatC: Format[C])(implicit ctag: ClassTag[C]): WriteFunc = { | |
case x: C => JsObject(Seq( | |
typeFieldName -> JsString(typeName), | |
dataFieldName -> formatC.writes(x) | |
)) | |
} | |
private def toTypeJsResult[B <: A](p: PartialFunction[String, B]): ReadsFunc[B] = typeFromJsValue(_).collect(error)(p) | |
private def writeSingleton[B <: A](singleton: B): WriteFunc = { | |
case `singleton` => JsObject(Seq( | |
typeFieldName -> (JsString compose initialToLower)(singleton.productPrefix) | |
)) | |
} | |
private def strToSingleton[B <: A](singleton: B): PartialFunction[String, B] = { | |
case (x: String) if x == initialToLower(singleton.productPrefix) => singleton | |
} | |
private def readSingleton[B <: A](s: B): ReadsFunc[B] = toTypeJsResult(strToSingleton(s)) | |
protected def formatSingleton[B <: A](singleton: B): Format[B] = Format[B]( | |
Reads[B](readSingleton(singleton)), | |
Writes[B](writeSingleton(singleton)) | |
) | |
protected def readSingletons(singletons: Seq[A]): ReadsFunc[A] = singletons match { | |
case Nil => _: JsValue => JsError("singletons must not be empty") | |
case s :: Nil => readSingleton(s) | |
case s :: ss => { | |
val initial: PartialFunction[String, A] = strToSingleton(s) | |
val op = (p: PartialFunction[String, A], x: A) => p.orElse(strToSingleton(x)) | |
val composedPartials = ss.foldLeft(initial)(op) | |
toTypeJsResult(composedPartials) | |
} | |
} | |
protected def writeSealedTrait(singletons: Seq[A]): WriteFunc = singletons match { | |
case Nil => { case _ => JsNull } | |
case s :: Nil => writeSingleton(s) | |
case s :: ss => { | |
val initial: WriteFunc = writeSingleton(s) | |
val op = (p: WriteFunc, x: A) => p.orElse(writeSingleton(x)) | |
ss.foldLeft(initial)(op) | |
} | |
} | |
protected def formatSealedTrait: Format[A] = Format[A]( | |
Reads(singletonReadFunc).orElse(Reads(nonSingletonReadFunc)), | |
Writes(writeFunc) | |
) | |
// Required to be override for serialization to work | |
implicit def format: Format[Fruit] | |
} | |
// AdtFormat usgae | |
import Fruit.format // scalastyle:ignore | |
sealed trait Fruit extends Product with Serializable | |
case object Pear extends Fruit | |
case object Apple extends Fruit | |
case class Banana(ripe: Boolean) extends Fruit | |
object Banana { | |
val format: Format[Banana] = Json.format | |
} | |
case class Orange(fresh: Boolean) extends Fruit | |
object Orange { | |
val format: Format[Orange] = Json.format | |
} | |
object Fruit extends AdtFormat[Fruit] { | |
val ORANGE = "orange" | |
val BANANA = "banana" | |
val fruits: Seq[Fruit] = Seq(Pear, Apple) | |
// By default not used | |
val appleFormat: Format[Apple.type] = formatSingleton(Apple) | |
// By default not used | |
val pearFormat: Format[Pear.type] = formatSingleton(Pear) | |
val nonSingletonReads = Seq( | |
readNonSingletonByType[Orange](ORANGE, Orange.format) _, | |
readNonSingletonByType[Banana](BANANA, Banana.format) _ | |
) | |
override def singletonReadFunc: ReadsFunc[Fruit] = readSingletons(fruits) | |
override def nonSingletonReadFunc: ReadsFunc[Fruit] = readNonSingletons(nonSingletonReads) | |
override def writeFunc: Fruit.WriteFunc = | |
writeSealedTrait(fruits) orElse | |
writeNonSingleton[Orange](ORANGE, Orange.format) orElse | |
writeNonSingleton[Banana](BANANA, Banana.format) orElse | |
writeNull | |
implicit override val format: Format[Fruit] = formatSealedTrait | |
} | |
// ADT serialization for case classes | |
val orange1 = Json.toJson(Orange(true)).toString() | |
//orange1: String = {"_type":"orange","_data":{"fresh":true}} | |
Json.parse(orange1) | |
//res0: play.api.libs.json.JsValue = {"_type":"orange","_data":{"fresh":true}} | |
Json.fromJson(Json.parse(orange1)).asOpt | |
//res1: Option[Fruit] = Some(Orange(true)) | |
// ADT serialization for case objects | |
val s1 = Json.toJson(Apple).toString() | |
//s1: String = {"_type":"apple"} | |
val js1 = Json.parse(s1) | |
//js1: play.api.libs.json.JsValue = {"_type":"apple"} | |
js1.as[Fruit] == Json.fromJson[Fruit](js1).get | |
//res2: Boolean = true | |
// ADT serialization for case objects | |
val s3 = Json.toJson(Pear) | |
//s3: play.api.libs.json.JsValue = {"_type":"pear"} | |
val js3 = Json.parse(s3.toString) | |
//js3: play.api.libs.json.JsValue = {"_type":"pear"} | |
s3 == js3 | |
//res3: Boolean = true | |
Json.fromJson(js3)(Fruit.format).asOpt | |
//res4: Option[Fruit] = Some(Pear) | |
// Additional singleton serialization for case objects with explicit format params | |
val s2 = Json.toJson(Apple)(Fruit.appleFormat).toString() | |
//s2: String = {"_type":"apple"} | |
Json.parse(s2).as[Apple.type](Fruit.appleFormat) | |
//res5: Apple.type = Apple | |
Json.fromJson(js1)(Fruit.appleFormat).asOpt | |
//res6: Option[Apple.type] = Some(Apple) | |
js1.as[Apple.type](Fruit.appleFormat) | |
//res7: Apple.type = Apple | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment