Last active
August 8, 2018 11:01
-
-
Save joprice/d9bad5ca2d2ef68fca73 to your computer and use it in GitHub Desktop.
Play json > tuple 21 macros
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
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) | |
} | |
} |
not found: value MacroHelpers
What lib should be imported ?
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
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.