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 => "" 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 } }