aboutsummaryrefslogblamecommitdiff
path: root/macros/src/main/scala/macro.scala
blob: a3f61dbfc51359541b14fdaffcf38b173781fab8 (plain) (tree)
1
2
3
4
5
6
7
8
9








                                              

                                                             






                                                                   

                                                                    

                            
                                  







                                                                    
                                


                                                                                             


                                                 
       







                                              



















                                                                                                                                                         


                                                          


                                                                      













                                     

                     




                                                                     








                                                                       



                                                                       

     

                                                                              
 



                                                                                                                                   
 
                                                                


                                                 






                                                                                                            

     


                                                                                   










                                                                                                            
package macros

//import scala.reflect.macros.whitebox.Context
import scala.reflect.macros.blackbox.Context

import scala.language.experimental.macros
import scala.language.higherKinds

trait ProductWriters[Writer[_]] {
  implicit def productWriter[P]: Writer[P] =
    macro ProductMapperBundle.productWriterImpl[Writer[_], P]
}

class ProductMapperBundle(val c: Context) {
  import c.universe._

  /** Summon an implicit value and return the tree representing its
    * invocation, or fail if no implicit is available. */
  private def implicitlyOrFail(tpe: Type, message: String): Tree = {
    c.typecheck(q"""{
        import ${c.prefix}._
        implicitly[$tpe]
      }""", silent = true) match {
      case EmptyTree => c.abort(c.enclosingPosition, message)
      case tree => tree
    }
  }

  private case class TypeClass(baseType: Type, method: MethodSymbol)

  private def writerTypeClass[T: c.WeakTypeTag]: TypeClass = {
    val baseType = weakTypeOf[T]
    val typeParam: Symbol = baseType.typeSymbol.asType.typeParams.head

    // extract methods that take one type parameter, matching the type classes type parameter
    val methods = baseType.decls
      .collect {
        case m: MethodSymbol if m.isAbstract => m
      }
      .filter { m =>
        m.paramLists match {
          case (param :: Nil) :: Nil =>
            param.info.typeSymbol == typeParam
          case _ => false
        }
      }
      .toList

    methods match {
      case head :: Nil => TypeClass(baseType, head)
      case list =>
        val message = list.map(_.name.toString).map(n => "<" + n + ">") match {
          case Nil => "<no abstract method>"
          case list => list.mkString(", ")
        }
        c.abort(
          c.prefix.tree.pos,
          s"${weakTypeOf[T]} must have exaclty one abstract method with conforming signature. It currently has the following abstract methods: $message."
        )
    }
  }

  /** Create a new writer.
    * @param tc the base writer type class
    * @param genericType the elemnt type of the new writer to create
    * @param body a function that generates the writer's body from a given parameter name
    */
  private def newWriter(tc: TypeClass,
                        genericType: Type,
                        body: TermName => Tree): Block = {
    val parent = appliedType(tc.baseType.typeConstructor, genericType)

    val paramName = TermName("value")
    val param = ValDef(
      Modifiers(Flag.PARAM),
      paramName,
      TypeTree(genericType),
      EmptyTree
    )
    val defn = DefDef(
      Modifiers(),
      tc.method.name,
      Nil,
      List(List(param)),
      TypeTree(tc.method.returnType),
      body(paramName)
    )

    val tree = Block(
      List(q"final class $$anon extends $parent { $defn }"),
      Apply(
        Select(New(Ident(TypeName("$anon"))), termNames.CONSTRUCTOR),
        List()
      )
    )
    tree
  }

  def productWriterImpl[W: c.WeakTypeTag, P: c.WeakTypeTag]: c.Tree = {
    val product = weakTypeOf[P]
    val writer: TypeClass = writerTypeClass[W]

    if (!(product <:< weakTypeOf[Product])) {
      c.abort(
        c.enclosingPosition,
        s"Cannot generate product writer for non-product type $product"
      )
    }

    val mapType: Type =
      appliedType(typeOf[Map[_, _]], typeOf[String], writer.method.returnType)

    val mapWriter = implicitlyOrFail(
      appliedType(writer.baseType, mapType),
      s"No implicit decomposer available for ${appliedType(writer.baseType, product)}. Make sure an implicit $mapType is in scope."
    )

    val fields: List[(TermName, Type)] = product.decls.collect {
      case m: MethodSymbol if m.isCaseAccessor =>
        m.name -> m.info.resultType
    }.toList
    val fieldWriters: List[(TermName, Tree)] = fields.map {
      case (name, tpe) =>
        val writerType = appliedType(writer.baseType, tpe)
        name -> implicitlyOrFail(
          writerType,
          s"Cannot create writer for $product: no implicit writer available for $product.$name of type $tpe"
        )
    }

    def mapBody(param: TermName): List[Tree] = fieldWriters.map {
      case (name, fieldWriter) =>
        q"""(${name.toString}, $fieldWriter.${writer.method.name}($param.$name))"""
    }

    def data(param: TermName): Tree = q"""{
      val data = scala.collection.immutable.Map(..${mapBody(param)}); $mapWriter.${writer.method.name}(data)
    }"""

    val tree = newWriter(writer, product, param => data(param))
    //println(tree)
    tree
  }
}