[info] Running com.github.fpopic.scalamacros.DefMacroUsage
Map(i -> 101, s -> Some(2), l -> List(3, 4), n -> Map(j -> 105, k -> 107))
// Would like to serialize any Scala case class (could be nested as well) to Map[String, Any] | |
case class N(j: Int) | |
case class A(i: Int, s: Some[Int], l: List[Int], n: N) | |
// Want to generate: | |
object N extends Mappable[N] { | |
def toMap(n: N): Map[String, Any] = { | |
Map( | |
"j" -> n.j | |
) | |
} | |
} | |
object A extends Mappable[A] { | |
def toMap(a: A): Map[String, Any] = { | |
Map( | |
"i" -> a.i, | |
"s" -> a.s, | |
"l" -> a.l, | |
"n" -> N.toMap(n) | |
) | |
} | |
} | |
/* | |
For later: | |
- before adding pair to map check if null | |
- convert Scala to Java collections | |
- support Maps as fields | |
- extract Some/Option | |
- convert Timestamp-like fields to String Literals | |
- add field customizations using annotations or some context (like customizationsMap) | |
*/ |
package com.github.fpopic.scalamacros | |
import scala.language.experimental.macros | |
import scala.reflect.macros.blackbox | |
// 0. Define Type Class | |
trait Mappable[T] { | |
def toMap(t: T): Map[String, Any] | |
} | |
object Mappable extends MappableLowPriorityImplicits { | |
// 2. Implicit method that triggers the macro | |
// HighPriorityImplicits | |
// LowPriorityMacros | |
implicit def caseClassMappable[T]: Mappable[T] = macro materializeMappableImpl[T] | |
// 1. Caller initiates type class implicits resolution | |
def mapify[T](t: T)(implicit m: Mappable[T]): Map[String, Any] = m.toMap(t) | |
} | |
trait MappableLowPriorityImplicits { | |
// 3. Macro that generates for any case class Mappable implementation | |
def materializeMappableImpl[T: c.WeakTypeTag](c: blackbox.Context): c.Expr[Mappable[T]] = { | |
import c.universe._ | |
val tpe: c.universe.Type = weakTypeOf[T] | |
println(s"Mappable: $tpe") | |
def getPrimaryConstructorMembers(_tpe: c.Type): Seq[c.Symbol] = | |
_tpe.decls.collectFirst { | |
case m: MethodSymbol if m.isPrimaryConstructor => m | |
}.get.paramLists.head | |
val mapEntries: Seq[c.Tree] = | |
getPrimaryConstructorMembers(tpe).map { field => | |
val fName = field.name.decodedName.toString | |
val fTerm = field.name.toTermName | |
// doesn't work with tType.decl(field.name).typeSignature | |
val fType = field.typeSignature | |
fType match { | |
case t if t =:= weakTypeOf[Int] => | |
println(s"$fName : $fType") | |
q"$fName -> (t.$fTerm + 100)" | |
case t if t =:= weakTypeOf[Some[Int]] => | |
println(s"$fName : $fType") | |
q"$fName -> t.$fTerm" | |
case t if t =:= weakTypeOf[List[Int]] => | |
println(s"$fName : $fType") | |
q"$fName -> t.$fTerm" | |
case n if n.baseClasses.contains(weakTypeOf[Product].typeSymbol) => | |
println(s"$fName : $fType") | |
q"$fName -> implicitly[Mappable[$n]].toMap(t.$fTerm)" | |
case t => | |
c.abort(c.enclosingPosition, s"Type $t not supported.") | |
} | |
} | |
val ret = | |
q"""new Mappable[$tpe] { | |
def toMap(t: $tpe): Map[String, Any] = Map(..$mapEntries) | |
} | |
""" | |
println(s"Ret: $ret") | |
c.Expr[Mappable[T]](ret) | |
} | |
} |
package com.github.fpopic.scalamacros | |
case class N(j: Int, k: Int) | |
case class A(i: Int, s: Some[Int], l: List[Int], n: N) | |
object DefMacroUsage { | |
def main(args: Array[String]): Unit = { | |
import Mappable._ | |
val a: Map[String, Any] = mapify( | |
A( | |
i = 1, | |
s = Some(2), | |
l = List(3, 4), | |
n = N( | |
j = 5, | |
k = 7 | |
) | |
) | |
) | |
println(a) | |
} | |
} |
[info] Compiling 4 Scala sources to /home/fpopic/Projects/IdeaProjects/github/fpopic/scala-code-generation-poc/scala-macros-usage/target/scala-2.13.0-M3/classes ... | |
Mappable: com.github.fpopic.scalamacros.A | |
i : Int | |
s : Some[Int] | |
l : List[Int] | |
n : com.github.fpopic.scalamacros.N | |
Ret: { | |
final class $anon extends Mappable[com.github.fpopic.scalamacros.A] { | |
def <init>() = { | |
super.<init>(); | |
() | |
}; | |
def toMap(t: com.github.fpopic.scalamacros.A): Map[String, Any] = Map("i".$minus$greater(t.i.$plus(100)), "s".$minus$greater(t.s), "l".$minus$greater(t.l), "n".$minus$greater(implicitly[Mappable[com.github.fpopic.scalamacros.N]].toMap(t.n))) | |
}; | |
new $anon() | |
} | |
Mappable: com.github.fpopic.scalamacros.N | |
j : Int | |
k : Int | |
Ret: { | |
final class $anon extends Mappable[com.github.fpopic.scalamacros.N] { | |
def <init>() = { | |
super.<init>(); | |
() | |
}; | |
def toMap(t: com.github.fpopic.scalamacros.N): Map[String, Any] = Map("j".$minus$greater(t.j.$plus(100)), "k".$minus$greater(t.k.$plus(100))) | |
}; | |
new $anon() | |
} | |
[info] Done compiling. | |
[success] Total time: 18 s, completed Jun 1, 2019 11:39:14 PM |
[info] Running com.github.fpopic.scalamacros.DefMacroUsage
Map(i -> 101, s -> Some(2), l -> List(3, 4), n -> Map(j -> 105, k -> 107))