aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--build.sbt14
-rw-r--r--macros/src/main/scala/macro.scala117
-rw-r--r--project/build.properties1
-rw-r--r--src/main/scala/Main.scala58
5 files changed, 191 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..9f97022
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+target/ \ No newline at end of file
diff --git a/build.sbt b/build.sbt
new file mode 100644
index 0000000..9065dda
--- /dev/null
+++ b/build.sbt
@@ -0,0 +1,14 @@
+name := "protomacro"
+
+scalaVersion in ThisBuild := "2.12.2"
+
+scalacOptions in ThisBuild ++= Seq("-feature", "-deprecation")
+
+lazy val root = (project in file("."))
+ .dependsOn(macros)
+ .settings(scalacOptions ++= Seq("-Xlog-implicits"))
+
+lazy val macros = (project in file("macros"))
+ .settings(
+ libraryDependencies += "org.scala-lang" % "scala-reflect" % scalaVersion.value
+ )
diff --git a/macros/src/main/scala/macro.scala b/macros/src/main/scala/macro.scala
new file mode 100644
index 0000000..87e0000
--- /dev/null
+++ b/macros/src/main/scala/macro.scala
@@ -0,0 +1,117 @@
+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
+ }
+}
diff --git a/project/build.properties b/project/build.properties
new file mode 100644
index 0000000..5f32afe
--- /dev/null
+++ b/project/build.properties
@@ -0,0 +1 @@
+sbt.version=0.13.13 \ No newline at end of file
diff --git a/src/main/scala/Main.scala b/src/main/scala/Main.scala
new file mode 100644
index 0000000..c19d3ec
--- /dev/null
+++ b/src/main/scala/Main.scala
@@ -0,0 +1,58 @@
+package macros
+
+import scala.collection.immutable.Map
+
+object Test extends App {
+
+ sealed trait JsValue
+ case class JsObject(fields: Map[String, JsValue]) extends JsValue
+ case class JsString(str: String) extends JsValue
+ case class JsNumber(num: Int) extends JsValue
+
+ trait MyWriter[A] {
+ def write(value: A): JsValue
+ def other(a: A): Int = 2
+ }
+ trait BaseWriters {
+ implicit val idWriter = new MyWriter[JsValue] {
+ def write(value: JsValue) = value
+ }
+ implicit val intWriter = new MyWriter[Int] {
+ def write(value: Int) = JsNumber(value)
+ }
+ implicit val stringWriter = new MyWriter[String] {
+ def write(value: String) = JsString(value)
+ }
+ }
+ trait CompositeWriters {
+ implicit def mapWriter[A: MyWriter] = new MyWriter[Map[String, A]] {
+ def write(mp: Map[String, A]) = {
+ def writeA(v: A) = implicitly[MyWriter[A]].write(v)
+ JsObject(mp.map { case (k, v) => k -> writeA(v) })
+ }
+ }
+ }
+
+ object Util {
+ def save[A: MyWriter](a: A): Unit = {
+ val x = implicitly[MyWriter[A]].write(a)
+ println("saving " + x)
+ }
+ }
+
+ case class Foo(x: Int)
+ case class Composite(x: Int, y: String, other: Foo)
+
+ object Protocol
+ extends BaseWriters
+ with CompositeWriters
+ with ProductWriters[MyWriter]
+ import Protocol._
+
+ class Bar
+ Util.save(2)
+ //Util.save(new Bar)
+ Util.save(Composite(2, "some string", Foo(3)))
+ //Util.save((2, "some string", "1")
+
+}