-
Notifications
You must be signed in to change notification settings - Fork 123
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #179 from twitter/jco/macro_case_class
Generate some useful case class conversions with macros
- Loading branch information
Showing
10 changed files
with
597 additions
and
2 deletions.
There are no files selected for viewing
12 changes: 12 additions & 0 deletions
12
...ion-macros-common/src/main/scala/com/twitter/bijection/macros/common/MacroImplicits.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package com.twitter.bijection.macros.common | ||
|
||
import scala.language.experimental.macros | ||
|
||
import com.twitter.bijection.macros.common.impl.MacroImpl | ||
|
||
object MacroImplicits { | ||
/** | ||
* This method provides proof that the given type is a case class. | ||
*/ | ||
implicit def isCaseClass[T]: IsCaseClass[T] = macro MacroImpl.isCaseClassImpl[T] | ||
} |
11 changes: 11 additions & 0 deletions
11
bijection-macros-common/src/main/scala/com/twitter/bijection/macros/common/Macros.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package com.twitter.bijection.macros.common | ||
|
||
/** | ||
* This trait is meant to be used exclusively to allow the type system to prove that a class is or is not a case class. | ||
*/ | ||
trait IsCaseClass[T] | ||
|
||
/** | ||
* This is a tag trait to allow macros to signal, in a uniform way, that a piece of code was generated. | ||
*/ | ||
trait MacroGenerated |
29 changes: 29 additions & 0 deletions
29
...ion-macros-common/src/main/scala/com/twitter/bijection/macros/common/impl/MacroImpl.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
package com.twitter.bijection.macros.common.impl | ||
|
||
import scala.language.experimental.macros | ||
import scala.reflect.macros.Context | ||
import scala.reflect.runtime.universe._ | ||
import scala.util.{ Try => BasicTry } | ||
|
||
import com.twitter.bijection.macros.common.{ IsCaseClass, MacroGenerated } | ||
|
||
object MacroImpl { | ||
def isCaseClassImpl[T](c: Context)(implicit T: c.WeakTypeTag[T]): c.Expr[IsCaseClass[T]] = { | ||
import c.universe._ | ||
if (isCaseClassType(c)(T.tpe)) { | ||
//TOOD we should support this, just need to make sure it is concrete | ||
if (T.tpe.typeConstructor.takesTypeArgs) { | ||
c.abort(c.enclosingPosition, "Case class with type parameters currently not supported") | ||
} else { | ||
c.Expr[IsCaseClass[T]](q"""_root_.com.twitter.bijection.macros.common.impl.MacroGeneratedIsCaseClass[$T]()""") | ||
} | ||
} else { | ||
c.abort(c.enclosingPosition, "Type parameter is not a case class") | ||
} | ||
} | ||
|
||
def isCaseClassType(c: Context)(tpe: c.universe.Type): Boolean = | ||
BasicTry { tpe.typeSymbol.asClass.isCaseClass }.toOption.getOrElse(false) | ||
} | ||
|
||
case class MacroGeneratedIsCaseClass[T]() extends IsCaseClass[T] with MacroGenerated |
38 changes: 38 additions & 0 deletions
38
...on-macros-common/src/test/scala/com/twitter/bijection/macros/common/MacroDepHygiene.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
package com.twitter.bijection.macros.common | ||
|
||
import org.scalatest.{ Matchers, WordSpec } | ||
import org.scalatest.exceptions.TestFailedException | ||
import com.twitter.bijection.macros.common.{ _ => _ } | ||
|
||
/** | ||
* This test is intended to ensure that the macros do not require any imported code in scope. This is why all | ||
* references are via absolute paths. | ||
*/ | ||
class MacroDepHygiene extends WordSpec with Matchers { | ||
import com.twitter.bijection.macros.common.MacroImplicits.isCaseClass | ||
|
||
case class A(x: Int, y: String) | ||
case class B(x: A, y: String, z: A) | ||
class C | ||
|
||
def isMg(t: AnyRef) { | ||
t shouldBe a[com.twitter.bijection.macros.common.MacroGenerated] | ||
canExternalize(t) | ||
} | ||
|
||
def canExternalize(t: AnyRef) { com.twitter.chill.Externalizer(t).javaWorks shouldBe true } | ||
|
||
"IsCaseClass macro" should { | ||
val dummy = new com.twitter.bijection.macros.common.IsCaseClass[Nothing] {} | ||
def isCaseClassAvailable[T](implicit proof: com.twitter.bijection.macros.common.IsCaseClass[T] = dummy.asInstanceOf[com.twitter.bijection.macros.common.IsCaseClass[T]]) { isMg(proof) } | ||
|
||
"work fine without any imports" in { | ||
isCaseClassAvailable[A] | ||
isCaseClassAvailable[B] | ||
} | ||
|
||
"fail if not available" in { | ||
a[TestFailedException] should be thrownBy isCaseClassAvailable[C] | ||
} | ||
} | ||
} |
17 changes: 17 additions & 0 deletions
17
bijection-macros/src/main/scala/com/twitter/bijection/macros/MacroImplicits.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
package com.twitter.bijection.macros | ||
|
||
import scala.language.experimental.macros | ||
|
||
import com.twitter.bijection._ | ||
import com.twitter.bijection.macros.common.IsCaseClass | ||
import com.twitter.bijection.macros.impl.MacroImpl | ||
|
||
trait LowerPriorityMacroImplicits { | ||
implicit def materializeCaseClassToTupleNonRecursive[T: IsCaseClass, Tup]: Bijection[T, Tup] = macro MacroImpl.caseClassToTupleImplNonRecursive[T, Tup] | ||
implicit def materializeCaseClassToMapNonRecursive[T: IsCaseClass]: Injection[T, Map[String, Any]] = macro MacroImpl.caseClassToMapImplNonRecursive[T] | ||
} | ||
object LowerPriorityMacroImplicits extends LowerPriorityMacroImplicits | ||
object MacroImplicits extends LowerPriorityMacroImplicits { | ||
implicit def materializeCaseClassToTuple[T: IsCaseClass, Tup]: Bijection[T, Tup] = macro MacroImpl.caseClassToTupleImpl[T, Tup] | ||
implicit def materializeCaseClassToMap[T: IsCaseClass]: Injection[T, Map[String, Any]] = macro MacroImpl.caseClassToMapImpl[T] | ||
} |
12 changes: 12 additions & 0 deletions
12
bijection-macros/src/main/scala/com/twitter/bijection/macros/Macros.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package com.twitter.bijection.macros | ||
|
||
import scala.language.experimental.macros | ||
|
||
import com.twitter.bijection._ | ||
import com.twitter.bijection.macros.common.IsCaseClass | ||
import com.twitter.bijection.macros.impl.MacroImpl | ||
|
||
object Macros { | ||
def caseClassToTuple[T: IsCaseClass, Tup](recursivelyApply: Boolean = true): Bijection[T, Tup] = macro MacroImpl.caseClassToTupleImplWithOption[T, Tup] | ||
def caseClassToMap[T: IsCaseClass](recursivelyApply: Boolean = true): Injection[T, Map[String, Any]] = macro MacroImpl.caseClassToMapImplWithOption[T] | ||
} |
164 changes: 164 additions & 0 deletions
164
bijection-macros/src/main/scala/com/twitter/bijection/macros/impl/MacroImpl.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
package com.twitter.bijection.macros.impl | ||
|
||
import scala.collection.mutable.{ Map => MMap } | ||
import scala.language.experimental.macros | ||
import scala.reflect.macros.Context | ||
import scala.reflect.runtime.universe._ | ||
import scala.util.Try | ||
|
||
import com.twitter.bijection._ | ||
import com.twitter.bijection.macros.common.{ IsCaseClass, MacroGenerated } | ||
import com.twitter.bijection.macros.common.impl.{ MacroImpl => CommonMacroImpl } | ||
|
||
object MacroImpl { | ||
def caseClassToTupleImplWithOption[T, Tup](c: Context)(recursivelyApply: c.Expr[Boolean])(proof: c.Expr[IsCaseClass[T]])(implicit T: c.WeakTypeTag[T], Tup: c.WeakTypeTag[Tup]): c.Expr[Bijection[T, Tup]] = { | ||
import c.universe._ | ||
recursivelyApply match { | ||
case q"""true""" => caseClassToTupleNoProofImpl(c)(T, Tup) | ||
case q"""false""" => caseClassToTupleNoProofImplNonRecursive(c)(T, Tup) | ||
} | ||
} | ||
|
||
def caseClassToTupleImpl[T, Tup](c: Context)(proof: c.Expr[IsCaseClass[T]])(implicit T: c.WeakTypeTag[T], Tup: c.WeakTypeTag[Tup]): c.Expr[Bijection[T, Tup]] = | ||
caseClassToTupleNoProofImpl(c)(T, Tup) | ||
|
||
def caseClassToTupleImplNonRecursive[T, Tup](c: Context)(proof: c.Expr[IsCaseClass[T]])(implicit T: c.WeakTypeTag[T], Tup: c.WeakTypeTag[Tup]): c.Expr[Bijection[T, Tup]] = | ||
caseClassToTupleNoProofImplNonRecursive(c)(T, Tup) | ||
|
||
def caseClassToTupleNoProofImplNonRecursive[T, Tup](c: Context)(implicit T: c.WeakTypeTag[T], Tup: c.WeakTypeTag[Tup]): c.Expr[Bijection[T, Tup]] = caseClassToTupleNoProofImplCommon(c, false)(T, Tup) | ||
|
||
def caseClassToTupleNoProofImpl[T, Tup](c: Context)(implicit T: c.WeakTypeTag[T], Tup: c.WeakTypeTag[Tup]): c.Expr[Bijection[T, Tup]] = caseClassToTupleNoProofImplCommon(c, true)(T, Tup) | ||
|
||
def caseClassToTupleNoProofImplCommon[T, Tup](c: Context, recursivelyApply: Boolean)(implicit T: c.WeakTypeTag[T], Tup: c.WeakTypeTag[Tup]): c.Expr[Bijection[T, Tup]] = { | ||
import c.universe._ | ||
|
||
val tupleCaseClassCache = MMap.empty[Type, Tree] | ||
|
||
//TODO pull out? | ||
def tupleCaseClassEquivalent(tpe: Type): Seq[Tree] = | ||
tpe.declarations.collect { | ||
case m: MethodSymbol if m.isCaseAccessor => | ||
m.returnType match { | ||
case tpe if CommonMacroImpl.isCaseClassType(c)(tpe) => | ||
tupleCaseClassCache.getOrElseUpdate(tpe, { | ||
val equiv = tupleCaseClassEquivalent(tpe) | ||
AppliedTypeTree(Ident(newTypeName("Tuple" + equiv.size)), equiv.toList) | ||
}) | ||
case tpe => Ident(tpe.typeSymbol.name.toTypeName) | ||
} | ||
}.toSeq | ||
|
||
val convCache = MMap.empty[Type, TermName] | ||
|
||
//TODO can make error handling better | ||
val companion = T.tpe.typeSymbol.companionSymbol | ||
val getPutConv = T.tpe | ||
.declarations | ||
.collect { case m: MethodSymbol if m.isCaseAccessor => m } | ||
.zip(tupleCaseClassEquivalent(T.tpe)) | ||
.zip(Tup.tpe.declarations.collect { case m: MethodSymbol if m.isCaseAccessor => m }) | ||
.zipWithIndex | ||
.map { | ||
case (((tM, treeEquiv), tupM), idx) => | ||
tM.returnType match { | ||
case tpe if recursivelyApply && CommonMacroImpl.isCaseClassType(c)(tpe) => | ||
val needDeclaration = !convCache.contains(tpe) | ||
val conv = convCache.getOrElseUpdate(tpe, newTermName("c2t_" + idx)) | ||
(q"""$conv.invert(tup.$tupM)""", | ||
q"""$conv(t.$tM)""", | ||
if (needDeclaration) Some(q"""val $conv = implicitly[_root_.com.twitter.bijection.Bijection[${tM.returnType}, $treeEquiv]]""") else None) // cache these | ||
case tpe => | ||
(q"""tup.$tupM""", | ||
q"""t.$tM""", | ||
None) | ||
} | ||
} | ||
|
||
val getters = getPutConv.map(_._1) | ||
val putters = getPutConv.map(_._2) | ||
val converters = getPutConv.flatMap(_._3) | ||
|
||
c.Expr[Bijection[T, Tup]](q""" | ||
_root_.com.twitter.bijection.macros.impl.MacroGeneratedBijection[$T,$Tup]( | ||
{ t: $T => | ||
..$converters | ||
(..$putters) | ||
}, | ||
{ tup: $Tup => | ||
..$converters | ||
$companion(..$getters) | ||
} | ||
) | ||
""") | ||
} | ||
|
||
def caseClassToMapImplWithOption[T](c: Context)(recursivelyApply: c.Expr[Boolean])(proof: c.Expr[IsCaseClass[T]])(implicit T: c.WeakTypeTag[T]): c.Expr[Injection[T, Map[String, Any]]] = { | ||
import c.universe._ | ||
recursivelyApply match { | ||
case q"""true""" => caseClassToMapNoProofImpl(c)(T) | ||
case q"""false""" => caseClassToMapNoProofImplNonRecursive(c)(T) | ||
} | ||
} | ||
|
||
def caseClassToMapImpl[T](c: Context)(proof: c.Expr[IsCaseClass[T]])(implicit T: c.WeakTypeTag[T]): c.Expr[Injection[T, Map[String, Any]]] = | ||
caseClassToMapNoProofImpl(c)(T) | ||
|
||
def caseClassToMapImplNonRecursive[T](c: Context)(proof: c.Expr[IsCaseClass[T]])(implicit T: c.WeakTypeTag[T]): c.Expr[Injection[T, Map[String, Any]]] = | ||
caseClassToMapNoProofImplNonRecursive(c)(T) | ||
|
||
def caseClassToMapNoProofImplNonRecursive[T](c: Context)(implicit T: c.WeakTypeTag[T]): c.Expr[Injection[T, Map[String, Any]]] = caseClassToMapNoProofImplCommon(c, false)(T) | ||
|
||
// TODO the only diff between this and the above is the case match and the converters. it's easy to gate this on the boolean | ||
def caseClassToMapNoProofImpl[T](c: Context)(implicit T: c.WeakTypeTag[T]): c.Expr[Injection[T, Map[String, Any]]] = caseClassToMapNoProofImplCommon(c, true)(T) | ||
|
||
def caseClassToMapNoProofImplCommon[T](c: Context, recursivelyApply: Boolean)(implicit T: c.WeakTypeTag[T]): c.Expr[Injection[T, Map[String, Any]]] = { | ||
import c.universe._ | ||
//TODO can make error handling better? | ||
val companion = T.tpe.typeSymbol.companionSymbol | ||
|
||
val getPutConv = T.tpe.declarations.collect { case m: MethodSymbol if m.isCaseAccessor => m }.zipWithIndex.map { | ||
case (m, idx) => | ||
val returnType = m.returnType | ||
val accStr = m.name.toTermName.toString | ||
returnType match { | ||
case tpe if recursivelyApply && CommonMacroImpl.isCaseClassType(c)(tpe) => | ||
val conv = newTermName("c2m_" + idx) | ||
(q"""$conv.invert(m($accStr).asInstanceOf[_root_.scala.collection.immutable.Map[String, Any]]).get""", | ||
q"""($accStr, $conv(t.$m))""", | ||
Some(q"""val $conv = implicitly[_root_.com.twitter.bijection.Injection[$tpe, _root_.scala.collection.immutable.Map[String, Any]]]""")) //TODO cache these | ||
case tpe => | ||
(q"""m($accStr).asInstanceOf[$returnType]""", | ||
q"""($accStr, t.$m)""", | ||
None) | ||
} | ||
} | ||
|
||
val getters = getPutConv.map(_._1) | ||
val putters = getPutConv.map(_._2) | ||
val converters = getPutConv.flatMap(_._3) | ||
|
||
c.Expr[Injection[T, Map[String, Any]]](q""" | ||
_root_.com.twitter.bijection.macros.impl.MacroGeneratedInjection[$T, _root_.scala.collection.immutable.Map[String, Any]]( | ||
{ t: $T => | ||
..$converters | ||
_root_.scala.collection.immutable.Map[String, Any](..$putters) | ||
}, | ||
{ m: _root_.scala.collection.immutable.Map[String, Any] => | ||
..$converters | ||
try { _root_.scala.util.Success($companion(..$getters)) } catch { case _root_.scala.util.control.NonFatal(e) => _root_.scala.util.Failure(e) } | ||
} | ||
) | ||
""") | ||
} | ||
} | ||
|
||
case class MacroGeneratedBijection[A, B](fn: A => B, inv: B => A) extends Bijection[A, B] with MacroGenerated { | ||
override def apply(a: A) = fn(a) | ||
override def invert(b: B) = inv(b) | ||
} | ||
case class MacroGeneratedInjection[A, B](fn: A => B, inv: B => Try[A]) extends Injection[A, B] with MacroGenerated { | ||
override def apply(a: A) = fn(a) | ||
override def invert(b: B) = inv(b) | ||
} | ||
|
||
//TODO test serialization of them |
Oops, something went wrong.