Listing the accessors of a case class is such a common operation when you're working with macros that I tend to keep a method like this around:
def accessors[A: u.WeakTypeTag](u: scala.reflect.api.Universe) = {
import u._
u.weakTypeOf[A].declarations.collect {
case acc: MethodSymbol if acc.isCaseAccessor => acc
}.toList
}
This will give us all the case class accessor method symbols for A
, if it has any. Note that I'm using the general reflection API here—there's no need to make this macro-specific yet.
We can wrap this method up with some other convenience stuff:
trait ReflectionUtils {
import scala.reflect.api.Universe
def accessors[A: u.WeakTypeTag](u: Universe) = {
import u._
u.weakTypeOf[A].declarations.collect {
case acc: MethodSymbol if acc.isCaseAccessor => acc
}.toList
}
def printfTree(u: Universe)(format: String, trees: u.Tree*) = {
import u._
Apply(
Select(reify(Predef).tree, "printf"),
Literal(Constant(format)) :: trees.toList
)
}
}
And now we can write the actual macro code pretty concisely:
trait Domain
object Macros extends ReflectionUtils {
import scala.language.experimental.macros
import scala.reflect.macros.Context
def log[D <: Domain](domain: D): Unit = macro log_impl[D]
def log_impl[D <: Domain: c.WeakTypeTag](c: Context)(domain: c.Expr[D]) = {
import c.universe._
if (!weakTypeOf[D].typeSymbol.asClass.isCaseClass) c.abort(
c.enclosingPosition,
"Need something typed as a case class!"
) else c.Expr(
Block(
accessors[D](c.universe).map(acc =>
printfTree(c.universe)(
"%s (%s) : %%s\n".format(
acc.name.decoded,
acc.typeSignature.typeSymbol.name.decoded
),
Select(domain.tree.duplicate, acc.name)
)
),
c.literalUnit.tree
)
)
}
}
Note that we still need to keep track of the specific case class type we're dealing with, but type inference will take care of that at the call site—we won't need to specify the type parameter explicitly.
Now we can open a REPL, paste in your case class definitions, and then write the following:
scala> Macros.log(Town("Washington, D.C.", 38.89, 77.03))
id (String) : Washington, D.C.
longitude (Double) : 38.89
latitude (Double) : 77.03
Or:
scala> Macros.log(Country("CH", "Switzerland"))
id (String) : CH
name (String) : Switzerland
As desired.