All Downloads are FREE. Search and download functionalities are using the official Maven repository.

no.kodeworks.kvarg.patch.CodecMacros.scala Maven / Gradle / Ivy

There is a newer version: 0.7
Show newest version
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