Skip to content

Instantly share code, notes, and snippets.

@q42jaap
Last active January 9, 2018 07:09
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save q42jaap/87b8c2c350baa4a44dbe to your computer and use it in GitHub Desktop.
Save q42jaap/87b8c2c350baa4a44dbe to your computer and use it in GitHub Desktop.
This Gist shows one way to create a macro that can read/write Json for a trait with implementations.
package util
import play.api.libs.json.Format
import util.macroimpl.MacrosImpl
import language.experimental.macros
object JsonMacros {
// We did not reuse \/ from scalaz, to avoid a dependency on scalaz in the macros module
trait \/[A, B]
def typedFormat[T, Opts <: _ \/ _]: Format[T] = macro MacrosImpl.typedFormat[T, Opts]
def typedWrites[T, Opts <: _ \/ _]: Format[T] = macro MacrosImpl.typedWrites[T, Opts]
def typedReads[T, Opts <: _ \/ _]: Format[T] = macro MacrosImpl.typedReads[T, Opts]
}
package util.macroimpl
import play.api.libs.json._
import _root_.util.JsonMacros.\/
import scala.reflect.macros.blackbox
object MacrosImpl {
def typedFormat[T: c.WeakTypeTag, Opts <: _ \/ _ : c.WeakTypeTag](c: blackbox.Context): c.Expr[Format[T]] = {
import c.universe._
val helper = new Helper[c.type, T, Opts](c)
val objExpr = c.Expr[T](Ident(TermName("obj")))
val jsonExpr = c.Expr[JsValue](Ident(TermName("json")))
reify {
new Format[T] {
override def reads(json: JsValue): JsResult[T] = helper.readsBody(jsonExpr).splice
override def writes(obj: T): JsValue = helper.writesBody(objExpr).splice
}
}
}
def typedReads[T: c.WeakTypeTag, Opts <: _ \/ _ : c.WeakTypeTag](c: blackbox.Context): c.Expr[Reads[T]] = {
import c.universe._
val helper = new Helper[c.type, T, Opts](c)
val jsonExpr = c.Expr[JsValue](Ident(TermName("json")))
reify {
new Reads[T] {
override def reads(json: JsValue): JsResult[T] = helper.readsBody(jsonExpr).splice
}
}
}
def typedWrites[T: c.WeakTypeTag, Opts <: _ \/ _ : c.WeakTypeTag](c: blackbox.Context): c.Expr[Writes[T]] = {
import c.universe._
val helper = new Helper[c.type, T, Opts](c)
val objExpr = c.Expr[T](Ident(TermName("obj")))
reify {
new Writes[T] {
override def writes(obj: T): JsValue = helper.writesBody(objExpr).splice
}
}
}
private class Helper[C <: blackbox.Context, T: C#WeakTypeTag, Opts <: _ \/ _ : C#WeakTypeTag](val c: C) {
import c.universe._
val TTypeName = c.weakTypeOf[T].typeSymbol.name
val optsType = c.weakTypeOf[Opts]
val unionType = c.typeOf[_ \/ _]
val writesType = typeOf[Writes[_]].typeConstructor
val readsType = typeOf[Reads[_]].typeConstructor
val unionTypes = parseUnionTypes(optsType)
val nonUniqueTypeNames = {
val typeNames = unionTypes.map(_.typeSymbol.name.toString)
typeNames.diff(typeNames.distinct).toSet
}
if (nonUniqueTypeNames.nonEmpty) {
c.abort(c.enclosingPosition, s"The classes ${nonUniqueTypeNames.mkString(",")} have name clashes for $TTypeName, cannot generate Writes[$TTypeName]")
}
/**
* Generates the body of the reads function.
*
* Generated code should look like this:
* {{{
* json \ "_type" match {
* case JsString("X") =>
* implicitly[Reads[X]](json)
* case JsString("Y") =>
* implicitly[Reads[Y]](json)
* ...
* case _ => JsError("_type '" + (json \ "_type") + "' invalid for " + MyTrait)
* }}}
*/
def readsBody(json: c.Expr[JsValue]): Expr[JsResult[T]] = {
val typePropertyExpr = q"""$json \ "_type""""
val TTypeNameLiteral = Literal(Constant(TTypeName.toString))
val errorMsgExpr = q""" "_type '" + $typePropertyExpr.toString + "' invalid for " + $TTypeNameLiteral """
val cases = (unionTypes map { typ =>
val typName = typ.typeSymbol.name
val typNameExpr = Literal(Constant(typName.toString))
val pattern = pq"JsString($typNameExpr)"
cq"$pattern => ${readBodyFromImplicit(json, typ)}"
}) :+ cq"""_ => JsError($errorMsgExpr)"""
val jsResultExpr = c.Expr[JsResult[T]](q"$typePropertyExpr match { case ..$cases }")
jsResultExpr
}
private def readBodyFromImplicit(json: c.Expr[JsValue], A: c.Type): c.Expr[JsResult[T]] = {
val ATypeName = A.typeSymbol.name
val readsA = c.inferImplicitValue(appliedType(readsType, List(A)))
if (readsA.isEmpty) {
c.abort(c.enclosingPosition, s"Could not find implicit Reads[$ATypeName]")
}
c.Expr[JsResult[T]](q"$readsA.reads($json)")
}
/**
* Generates the body of the writes function.
*
* Generated code should look like this:
*
* {{{
* obj match {
* case impl: X =>
* implicitly[Writes[X]].write(x) match {
* case obj: JsObject => obj + ("_type", "X")
* case _ => sys.error("Writes[X].write() did not return a JsObject")
* }
* case impl: Y => ....
* }
* }}}
*
* The pattern match will be checked for exhaustiveness by the compiler, issuing a warning at compile time.
*/
def writesBody(obj: c.Expr[T]): c.Expr[JsValue] = {
// Make the `case`s for the match
val cases = unionTypes map { typ =>
val implValName = TermName("impl")
val pattern = pq"$implValName: $typ"
cq"$pattern => ${writeBodyFromImplicit(implValName, typ)}"
}
// execute match on obj which results in serialized json with _type property.
val resultJsonExpr = c.Expr[JsValue](q"$obj match { case ..$cases }")
resultJsonExpr
}
/**
* When we've found out the type of T we're going to writes is actually an A, this function will generate code
* so that the implicit Writes[A] is used to generate the JsValue for the A.
*/
private def writeBodyFromImplicit(aValName: TermName, A: c.Type): c.Expr[JsValue] = {
val ATypeName = A.typeSymbol.name
val writesA = c.inferImplicitValue(appliedType(writesType, List(A)))
if (writesA.isEmpty) {
c.abort(c.enclosingPosition, s"Could not find implicit Writes[$ATypeName]")
}
val aJsValue = c.Expr[JsValue](q"$writesA.writes($aValName)")
// the name of the type A, without package, so be sure the names are unique
val ATypeNameExpr = c.Expr[String](Literal(Constant(ATypeName.toString)))
reify {
aJsValue.splice match {
case aJsObj: JsObject =>
aJsObj +("_type", JsString(ATypeNameExpr.splice))
case _ =>
sys.error(s"Writes[${ATypeNameExpr.splice}].write() did not return a JsObject")
}
}
}
/**
* Parses the type expression `A \/ B \/ C` t a list of types.
*
* A \/ B is short for \/[A, B], but infix notation is super helpful here.
* A \/ B \/ C is actually `\/[A, \/[B, C]]`, so the function works recursively.
*/
private def parseUnionTypes(tree: Type): List[Type] = {
if (tree <:< unionType) {
tree match {
case TypeRef(_, _, List(a, b)) => parseUnionTypes(a) ::: parseUnionTypes(b)
}
} else {
List(tree)
}
}
}
}
/**
* Feel free to use this code however you like.
*
* This Gist shows one way to create a macro that can read/write Json for a trait with implementations.
*
* Suppose we have the following types:
*/
sealed trait T
case class A(a: Int) extends T
case class B(b: String) extends T
/**
* Then trying to serialize a T, an implementation must choose to use a Writes[A] or Writes[B] depending on the runtime
* type of the object begin serialized.
*
* The JsonMacros.typedWrites can be told for which types to generate a pattern match. Using implicit lookups, it finds the
* specific Writes[_] for the needed types.
*/
object T {
import play.api.libs.json._
import util.JsonMacros._
implicit val jsonFormatA = Json.format[A]
implicit val jsonFormatB = Json.format[B]
implicit val jsonFormatT = util.JsonMacros.typedFormat[T, A \/ B]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment