no.kodeworks.kvarg.patch.CodecMacros.scala Maven / Gradle / Ivy
package no.kodeworks.kvarg.patch
import io.circe.{Decoder, Encoder, ObjectEncoder}
import scala.reflect.macros.blackbox
class CodecMacros(val c: blackbox.Context) {
import c.universe._
private[this] val encoderSymbol: Symbol = c.symbolOf[Encoder.type]
private[this] val decoderSymbol: Symbol = c.symbolOf[Decoder.type]
private[this] val encoderTC: Type = typeOf[Encoder[_]].typeConstructor
private[this] val decoderTC: Type = typeOf[Decoder[_]].typeConstructor
private[this] val poptionTC: Type = typeOf[Poption[_]].typeConstructor
private[this] case class Instance(tc: Type, tpe: Type, name: TermName) {
def resolve(): Tree = c.inferImplicitValue(appliedType(tc, List(appliedType(poptionTC, tpe)))) match {
case EmptyTree => c.abort(c.enclosingPosition, s"Could not find implicit $tc[$poptionTC[$tpe]]")
case instance => instance
}
}
private[this] case class Instances(encoder: Instance, decoder: Instance)
private[this] case class Member(name: TermName, decodedName: String, tpe: Type)
private[this] case class ProductRepr(members: List[Member]) {
val instances: List[Instances] =
members.foldLeft(List.empty[Instances]) {
case (acc, m@Member(_, _, tpe)) if acc.find(_.encoder.tpe =:= tpe).isEmpty =>
val instances = Instances(
Instance(encoderTC, tpe, TermName(c.freshName("encoder"))),
Instance(decoderTC, tpe, TermName(c.freshName("decoder")))
)
instances :: acc
case (acc, _) => acc
}.reverse
private[this] def fail(tpe: Type): Nothing = c.abort(c.enclosingPosition, s"Invalid instance lookup for $tpe")
def encoder(tpe: Type): Instance = instances.map(_.encoder).find(_.tpe =:= tpe).getOrElse(fail(tpe))
def decoder(tpe: Type): Instance = instances.map(_.decoder).find(_.tpe =:= tpe).getOrElse(fail(tpe))
}
private[this] def membersFromPrimaryConstr(tpe: Type): Option[List[Member]] = tpe.decls.collectFirst {
case m: MethodSymbol if m.isPrimaryConstructor => m.paramLists.flatten.map { field =>
val asf = tpe.decl(field.name).asMethod.returnType.asSeenFrom(tpe, tpe.typeSymbol)
Member(field.name.toTermName, field.name.decodedName.toString, asf)
}
}
private[this] def productRepr(tpe: Type): Option[ProductRepr] =
membersFromPrimaryConstr(tpe).map(ProductRepr(_))
private[this] def fail(tpe: Type): Nothing = c.abort(
c.enclosingPosition,
s"Could not identify primary constructor for $tpe"
)
private[this] def checkValSafety(owner: Symbol)(tree: Tree): Boolean = tree match {
case q"$f(...$x)" if x.nonEmpty => checkValSafety(owner)(f) && x.forall(_.forall(checkValSafety(owner)))
case x if x.isTerm => x.symbol.owner == owner
case _ => false
}
private[this] def checkEncoderValSafety(tree: Tree): Boolean = checkValSafety(encoderSymbol)(tree)
private[this] def checkDecoderValSafety(tree: Tree): Boolean = checkValSafety(decoderSymbol)(tree)
def materializeDecoder[T: c.WeakTypeTag]: c.Expr[Decoder[Patch[T]]] = materializeDecoderImpl[T]
def materializeEncoder[T: c.WeakTypeTag]: c.Expr[ObjectEncoder[Patch[T]]] = materializeEncoderImpl[T]
private[this] def materializeDecoderImpl[T: c.WeakTypeTag]: c.Expr[Decoder[Patch[T]]] = {
val tpe = weakTypeOf[T]
productRepr(tpe).fold(fail(tpe)) { repr =>
val patcher = q"_root_.no.kodeworks.kvarg.patch.Patcher[$tpe]"
if (repr.members.isEmpty) {
c.Expr[Decoder[Patch[T]]](q"_root_.io.circe.Decoder.const(_root_.no.kodeworks.kvarg.patch.Patch[$tpe](Map.empty, $patcher))")
} else {
val instanceDefs: List[Tree] = repr.instances.map(_.decoder).map {
case instance@Instance(_, instanceType, name) =>
val resolved = instance.resolve()
if (checkDecoderValSafety(resolved)) {
q"private[this] val $name: _root_.io.circe.Decoder[_root_.no.kodeworks.kvarg.patch.Poption[$instanceType]] = $resolved"
} else {
q"private[this] def $name: _root_.io.circe.Decoder[_root_.no.kodeworks.kvarg.patch.Poption[$instanceType]] = $resolved"
}
}
def decode(member: Member): Tree =
q"${repr.decoder(member.tpe).name}.tryDecode(c.downField(${member.decodedName}))"
val forComp = repr.members.map { member =>
fq"${member.name} <- ${decode(member)}"
}
val memberToPoptions = repr.members.map(member => q"${Symbol(member.name.toString)} -> ${member.name}")
val forYield =
q"""for (..$forComp) yield _root_.no.kodeworks.kvarg.patch.Patch[$tpe](Map(..$memberToPoptions), $patcher)"""
val patchDecoder =
q"""new _root_.io.circe.Decoder[_root_.no.kodeworks.kvarg.patch.Patch[$tpe]] {
..$instanceDefs
final def apply(c: _root_.io.circe.HCursor) = $forYield
}"""
c.Expr[Decoder[Patch[T]]](patchDecoder)
}
}
}
private[this] def materializeEncoderImpl[T: c.WeakTypeTag]: c.Expr[ObjectEncoder[Patch[T]]] = {
val tpe = weakTypeOf[T]
productRepr(tpe).fold(fail(tpe)) { repr =>
val instanceDefs: List[Tree] = repr.instances.map(_.encoder).map {
case instance@Instance(_, instanceType, name) =>
val resolved = instance.resolve()
if (checkEncoderValSafety(resolved)) {
q"private[this] val $name: _root_.io.circe.Encoder[_root_.no.kodeworks.kvarg.patch.Poption[$instanceType]] = $resolved"
}
else {
q"private[this] def $name: _root_.io.circe.Encoder[_root_.no.kodeworks.kvarg.patch.Poption[$instanceType]] = $resolved"
}
}
val fields: List[Tree] = repr.members.map {
case m@Member(_, decodedName, tpe) =>
repr.encoder(tpe) match {
case Instance(_, _, instanceName) => q"""(
$decodedName,
$instanceName.asInstanceOf[_root_.io.circe.Encoder[_root_.no.kodeworks.kvarg.patch.Poption[$tpe]]].apply(
a.poptions(_root_.scala.Symbol($decodedName)).asInstanceOf[_root_.no.kodeworks.kvarg.patch.Poption[$tpe]])
)"""
}
}
c.Expr[ObjectEncoder[Patch[T]]](
q"""
new _root_.io.circe.ObjectEncoder[_root_.no.kodeworks.kvarg.patch.Patch[$tpe]] {
..$instanceDefs
final def encodeObject(a: _root_.no.kodeworks.kvarg.patch.Patch[$tpe]): _root_.io.circe.JsonObject =
_root_.io.circe.JsonObject.fromIterable($fields)
}
"""
)
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy