Skip to content

Instantly share code, notes, and snippets.

@joprice
Last active August 8, 2018 11:01
Show Gist options
  • Save joprice/d9bad5ca2d2ef68fca73 to your computer and use it in GitHub Desktop.
Save joprice/d9bad5ca2d2ef68fca73 to your computer and use it in GitHub Desktop.
Play json > tuple 21 macros
import scala.language.experimental.macros
import scala.reflect.macros.blackbox.Context
import play.api.libs.json.{Format, Writes, Reads, OWrites}
object PlayJson{
def reads[A <: AnyRef]: Reads[A] = macro readsImpl[A]
def readsImpl[A <: AnyRef : c.WeakTypeTag](c: Context): c.Expr[Reads[A]] = {
import c.universe._
val typeTag = weakTypeOf[A]
val symbol = weakTypeOf[A].typeSymbol
val declarations = weakTypeOf[A].decls
val ctor = declarations.collectFirst { case m: MethodSymbol if m.isPrimaryConstructor => m }.get
val params = ctor.paramLists.head
val groupSize = 21
val withNames = params.grouped(groupSize).map { group =>
c.freshName() -> group
}.toMap
def resultTerm(groupName: String) = TermName(s"${groupName}Result")
val result = q"""
${symbol.name.toTermName}.apply(
..${withNames.flatMap { case (groupName, group) =>
(1 to group.size).map { (i: Int) =>
val term = resultTerm(groupName)
val field = TermName(s"_$i")
q"$term.$field"
}
}}
)
"""
def buildIndividualReads(groupName: String, group: List[Symbol]) = {
def isOption(s: Symbol) = {
s.typeSignature.typeConstructor <:< typeOf[Option[_]].typeConstructor
}
def method(s: Symbol) = TermName(if(isOption(s)) "readNullable" else "read")
def typeSignature(s: Symbol) = {
val signature = s.asTerm.typeSignature
if (isOption(s)) signature.typeArgs.head else signature
}
if (group.size == 1) {
val field = group.head
val key = field.name.decodedName.toString
q"""
val ${TermName(groupName)} = (
(__ \ $key).${method(field)}[${typeSignature(field)}].map(Tuple1(_))
)"""
} else {
q"""
val ${TermName(groupName)} = (
..${
group.map { field =>
val key = field.name.decodedName.toString
//val typeSignature = field.asTerm.typeSignature
q"(__ \ $key).${method(field)}[${typeSignature(field)}]"
}.reduce((a, b) => q"$a and $b")
}
).tupled
"""
}
}
def assembleResults = {
def buildArguments(groupName: String, group: List[Symbol]): Tree = {
val term = resultTerm(groupName)
val types = group.map(_.typeSignature)
val resultType = tq"(..$types)"
q"val $term: $resultType"
}
def loop(groups: List[(String, List[Symbol])]): Tree = groups match {
case Nil => throw new IllegalStateException("This should never happen")
// handling single type as special case, since Tuple1 is treated as a single unwrapped parameter
case (groupName, head ::Nil) :: Nil =>
val term = resultTerm(groupName)
val `type` = head.typeSignature
val resultType = tq"Tuple1[${`type`}]"
val arguments = q"val $term: $resultType"
val function = q"""(..$arguments) => $result"""
q"""${TermName(groupName)}.map($function)"""
case (groupName, group) :: Nil =>
val arguments = buildArguments(groupName, group)
val function = q"""(..$arguments) => $result"""
q"""${TermName(groupName)}.map($function)"""
case (groupName, group) :: rest =>
val arguments = buildArguments(groupName, group)
val function = q"""(..$arguments) => ${loop(rest)}"""
q"""${TermName(groupName)}.flatMap($function)"""
}
loop(withNames.toList)
}
val prepareReads = withNames.map { case (name, group) => buildIndividualReads(name, group) }
val tree = q"""
{
import play.api.libs.json._
import play.api.libs.functional.syntax._
..$prepareReads
..$assembleResults
}
"""
c.Expr[Reads[A]](tree)
}
def writes[A <: AnyRef]: OWrites[A] = macro writesImpl[A]
def writesImpl[A <: AnyRef : c.WeakTypeTag](c: Context): c.Expr[OWrites[A]] = {
import c.universe._
val symbol = weakTypeOf[A].typeSymbol
val tpe = weakTypeOf[A]
val declarations = tpe.decls
val ctor = declarations.collectFirst { case m: MethodSymbol if m.isPrimaryConstructor => m }.get
val params = ctor.paramLists.head
val tree = q"""
OWrites { entity: ${symbol} =>
Json.obj(
..${params.map { field =>
val key = field.name.decodedName.toString
val value = field.name.toTermName
q"""$key -> entity.$value"""
}}
)
}
"""
c.Expr[OWrites[A]](tree)
}
}
@joprice
Copy link
Author

joprice commented Sep 17, 2014

Two problems identified with this - not handling Option[T] as readNullable[T], and single tuple types not being generated by tq"" when it's passed a single type. Will update soon with fixes.

@mgosk
Copy link

mgosk commented Sep 23, 2014

not found: value MacroHelpers
What lib should be imported ?

@joprice
Copy link
Author

joprice commented Sep 25, 2014

It's a series of helpers that I was using, but aren't necessary here. I removed and updated with the fixes mentioned above. There's some repetition now that could be factored out.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment