Skip to content

Instantly share code, notes, and snippets.

@realpeterz
Created February 14, 2018 19:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save realpeterz/aecb53a67fb723485eb66d544a67d580 to your computer and use it in GitHub Desktop.
Save realpeterz/aecb53a67fb723485eb66d544a67d580 to your computer and use it in GitHub Desktop.
Generic Play Json ADT Format (Writes & Reads)
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