Last active
January 9, 2018 07:09
-
-
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.
This file contains hidden or 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
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] | |
} |
This file contains hidden or 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
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) | |
} | |
} | |
} | |
} |
This file contains hidden or 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
/** | |
* 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