diff --git a/bijection-macros-common/src/main/scala/com/twitter/bijection/macros/common/MacroImplicits.scala b/bijection-macros-common/src/main/scala/com/twitter/bijection/macros/common/MacroImplicits.scala deleted file mode 100644 index dc3a1678d..000000000 --- a/bijection-macros-common/src/main/scala/com/twitter/bijection/macros/common/MacroImplicits.scala +++ /dev/null @@ -1,12 +0,0 @@ -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] -} diff --git a/bijection-macros-common/src/test/scala/com/twitter/bijection/macros/common/MacroDepHygiene.scala b/bijection-macros-common/src/test/scala/com/twitter/bijection/macros/common/MacroDepHygiene.scala deleted file mode 100644 index 57bc9006d..000000000 --- a/bijection-macros-common/src/test/scala/com/twitter/bijection/macros/common/MacroDepHygiene.scala +++ /dev/null @@ -1,38 +0,0 @@ -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] - } - } -} diff --git a/bijection-macros-common/src/main/scala/com/twitter/bijection/macros/common/Macros.scala b/bijection-macros/src/main/scala/com/twitter/bijection/macros/MacroAnnotations.scala similarity index 54% rename from bijection-macros-common/src/main/scala/com/twitter/bijection/macros/common/Macros.scala rename to bijection-macros/src/main/scala/com/twitter/bijection/macros/MacroAnnotations.scala index 1db4dbcba..b0cdf6260 100644 --- a/bijection-macros-common/src/main/scala/com/twitter/bijection/macros/common/Macros.scala +++ b/bijection-macros/src/main/scala/com/twitter/bijection/macros/MacroAnnotations.scala @@ -1,8 +1,15 @@ -package com.twitter.bijection.macros.common +package com.twitter.bijection.macros + +import scala.language.experimental.macros +import com.twitter.bijection.macros.impl._ /** * 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. */ +object IsCaseClass { + implicit def isCaseClass[T]: IsCaseClass[T] = macro IsCaseClassImpl.isCaseClassImpl[T] +} + trait IsCaseClass[T] /** diff --git a/bijection-macros/src/main/scala/com/twitter/bijection/macros/MacroImplicits.scala b/bijection-macros/src/main/scala/com/twitter/bijection/macros/MacroImplicits.scala index 1865e6c67..0d7ac9ebe 100644 --- a/bijection-macros/src/main/scala/com/twitter/bijection/macros/MacroImplicits.scala +++ b/bijection-macros/src/main/scala/com/twitter/bijection/macros/MacroImplicits.scala @@ -3,15 +3,19 @@ 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 +import com.twitter.bijection.macros.impl.{ CaseClassToTuple, CaseClassToMap, IsCaseClassImpl } 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] + // Since implicit macro's aborting makes them just appear to never having existed + // its valid for this to be lower priority for the one in MacroImplicits. + // We will attempt this one if we don't see the other + implicit def materializeCaseClassToTupleNonRecursive[T: IsCaseClass, Tup]: Bijection[T, Tup] = macro CaseClassToTuple.caseClassToTupleImplNonRecursive[T, Tup] } -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] + + implicit def materializeCaseClassToTuple[T: IsCaseClass, Tup]: Bijection[T, Tup] = macro CaseClassToTuple.caseClassToTupleImpl[T, Tup] + implicit def materializeCaseClassToMap[T: IsCaseClass]: Injection[T, Map[String, Any]] = macro CaseClassToMap.caseClassToMapImpl[T] + + implicit def isCaseClass[T]: IsCaseClass[T] = macro IsCaseClassImpl.isCaseClassImpl[T] } diff --git a/bijection-macros/src/main/scala/com/twitter/bijection/macros/Macros.scala b/bijection-macros/src/main/scala/com/twitter/bijection/macros/Macros.scala index 7d3f4c957..78f5147b1 100644 --- a/bijection-macros/src/main/scala/com/twitter/bijection/macros/Macros.scala +++ b/bijection-macros/src/main/scala/com/twitter/bijection/macros/Macros.scala @@ -3,10 +3,9 @@ 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 +import com.twitter.bijection.macros.impl.{ CaseClassToTuple, CaseClassToMap } 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] + def caseClassToTuple[T: IsCaseClass, Tup](recursivelyApply: Boolean): Bijection[T, Tup] = macro CaseClassToTuple.caseClassToTupleImplWithOption[T, Tup] + def caseClassToMap[T: IsCaseClass](recursivelyApply: Boolean): Injection[T, Map[String, Any]] = macro CaseClassToMap.caseClassToMapImplWithOption[T] } diff --git a/bijection-macros/src/main/scala/com/twitter/bijection/macros/impl/CaseClassToMap.scala b/bijection-macros/src/main/scala/com/twitter/bijection/macros/impl/CaseClassToMap.scala new file mode 100644 index 000000000..5bf04d85e --- /dev/null +++ b/bijection-macros/src/main/scala/com/twitter/bijection/macros/impl/CaseClassToMap.scala @@ -0,0 +1,72 @@ +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.{ IsCaseClass, MacroGenerated } + +private[bijection] object CaseClassToMap { + 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.tree 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 && IsCaseClassImpl.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""" + new Injection[$T, _root_.scala.collection.immutable.Map[String, Any]] with MacroGenerated { + override def apply(t: $T): _root_.scala.collection.immutable.Map[String, Any] = { + ..$converters + _root_.scala.collection.immutable.Map[String, Any](..$putters) + } + override def invert(m: _root_.scala.collection.immutable.Map[String, Any]): _root_.scala.util.Try[ $T ] = { + ..$converters + try { _root_.scala.util.Success($companion(..$getters)) } catch { case _root_.scala.util.control.NonFatal(e) => _root_.scala.util.Failure(e) } + } + } + """) + + } +} diff --git a/bijection-macros/src/main/scala/com/twitter/bijection/macros/impl/CaseClassToTuple.scala b/bijection-macros/src/main/scala/com/twitter/bijection/macros/impl/CaseClassToTuple.scala new file mode 100644 index 000000000..6f9ad9241 --- /dev/null +++ b/bijection-macros/src/main/scala/com/twitter/bijection/macros/impl/CaseClassToTuple.scala @@ -0,0 +1,86 @@ +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.{ IsCaseClass, MacroGenerated } + +private[bijection] object CaseClassToTuple { + 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) + case _ => caseClassToTupleNoProofImpl(c)(T, Tup) + } + } + + // Entry point + 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 tupUtils = new TupleUtils[c.type](c) + 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(tupUtils.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 && IsCaseClassImpl.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""" + new Bijection[$T,$Tup] with MacroGenerated { + override def apply(t: $T): $Tup = { + ..$converters + (..$putters) + } + override def invert(tup: $Tup): $T = { + ..$converters + $companion(..$getters) + } + } + """) + } +} +//TODO test serialization of them diff --git a/bijection-macros-common/src/main/scala/com/twitter/bijection/macros/common/impl/MacroImpl.scala b/bijection-macros/src/main/scala/com/twitter/bijection/macros/impl/IsCaseClassImpl.scala similarity index 82% rename from bijection-macros-common/src/main/scala/com/twitter/bijection/macros/common/impl/MacroImpl.scala rename to bijection-macros/src/main/scala/com/twitter/bijection/macros/impl/IsCaseClassImpl.scala index c1ffb5b32..f995dcb52 100644 --- a/bijection-macros-common/src/main/scala/com/twitter/bijection/macros/common/impl/MacroImpl.scala +++ b/bijection-macros/src/main/scala/com/twitter/bijection/macros/impl/IsCaseClassImpl.scala @@ -1,13 +1,13 @@ -package com.twitter.bijection.macros.common.impl +package com.twitter.bijection.macros.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 } +import com.twitter.bijection.macros.{ IsCaseClass, MacroGenerated } -object MacroImpl { +private[bijection] object IsCaseClassImpl { def isCaseClassImpl[T](c: Context)(implicit T: c.WeakTypeTag[T]): c.Expr[IsCaseClass[T]] = { import c.universe._ if (isCaseClassType(c)(T.tpe)) { @@ -15,7 +15,7 @@ object MacroImpl { 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]()""") + c.Expr[IsCaseClass[T]](q"""_root_.com.twitter.bijection.macros.impl.MacroGeneratedIsCaseClass[$T]()""") } } else { c.abort(c.enclosingPosition, "Type parameter is not a case class") diff --git a/bijection-macros/src/main/scala/com/twitter/bijection/macros/impl/MacroImpl.scala b/bijection-macros/src/main/scala/com/twitter/bijection/macros/impl/MacroImpl.scala deleted file mode 100644 index edb6eb1b1..000000000 --- a/bijection-macros/src/main/scala/com/twitter/bijection/macros/impl/MacroImpl.scala +++ /dev/null @@ -1,164 +0,0 @@ -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 diff --git a/bijection-macros/src/main/scala/com/twitter/bijection/macros/impl/TupleUtils.scala b/bijection-macros/src/main/scala/com/twitter/bijection/macros/impl/TupleUtils.scala new file mode 100644 index 000000000..f12578780 --- /dev/null +++ b/bijection-macros/src/main/scala/com/twitter/bijection/macros/impl/TupleUtils.scala @@ -0,0 +1,25 @@ +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._ + +private[bijection] class TupleUtils[C <: Context](val c: C) { + import c.universe._ + private[this] val tupleCaseClassCache = MMap.empty[Type, Tree] + + // Takes a case class and generates the equiv tuple to it + def tupleCaseClassEquivalent(tpe: Type): Seq[Tree] = + tpe.declarations.collect { + case m: MethodSymbol if m.isCaseAccessor => + m.returnType match { + case tpe if IsCaseClassImpl.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 +} diff --git a/bijection-macros/src/test/scala/com/twitter/bijection/macros/MacroDepHygiene.scala b/bijection-macros/src/test/scala/com/twitter/bijection/macros/MacroDepHygiene.scala new file mode 100644 index 000000000..caa123a48 --- /dev/null +++ b/bijection-macros/src/test/scala/com/twitter/bijection/macros/MacroDepHygiene.scala @@ -0,0 +1,30 @@ +package com.twitter.bijection.macros + +import org.scalatest.{ Matchers, WordSpec } +import org.scalatest.exceptions.TestFailedException + +/** + * 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 with MacroTestHelper { + import com.twitter.bijection.macros.MacroImplicits.isCaseClass + import MacroCaseClasses._ + + "IsCaseClass macro" should { + val dummy = new com.twitter.bijection.macros.IsCaseClass[Nothing] {} + def isCaseClassAvailable[T](implicit proof: com.twitter.bijection.macros.IsCaseClass[T] = dummy.asInstanceOf[com.twitter.bijection.macros.IsCaseClass[T]]) { + proof shouldBe a[MacroGenerated] + canExternalize(proof) + } + + "work fine without any imports" in { + isCaseClassAvailable[SampleClassA] + isCaseClassAvailable[SampleClassB] + } + + "fail if not available" in { + a[TestFailedException] should be thrownBy isCaseClassAvailable[SampleClassD] + } + } +} diff --git a/bijection-macros/src/test/scala/com/twitter/bijection/macros/MacroPropTests.scala b/bijection-macros/src/test/scala/com/twitter/bijection/macros/MacroPropTests.scala index 2ec102082..e75dbaed0 100644 --- a/bijection-macros/src/test/scala/com/twitter/bijection/macros/MacroPropTests.scala +++ b/bijection-macros/src/test/scala/com/twitter/bijection/macros/MacroPropTests.scala @@ -8,149 +8,147 @@ import org.scalatest.prop.PropertyChecks import com.twitter.bijection._ import com.twitter.bijection.macros._ -import com.twitter.bijection.macros.common.{ MacroImplicits => CommonMacroImplicits, _ } import com.twitter.chill.Externalizer trait MacroPropTests extends PropSpec with PropertyChecks with Matchers with MacroTestHelper { import MacroImplicits._ - import CommonMacroImplicits._ import MacroCaseClasses._ //TODO make a macro to autogenerate arbitraries for case classes - implicit def arbA: Arbitrary[A] = Arbitrary[A] { + implicit def arbA: Arbitrary[SampleClassA] = Arbitrary[SampleClassA] { for ( a <- Arbitrary.arbInt.arbitrary; b <- Arbitrary.arbString.arbitrary - ) yield A(a, b) + ) yield SampleClassA(a, b) } - implicit def arbB: Arbitrary[B] = Arbitrary[B] { + implicit def arbB: Arbitrary[SampleClassB] = Arbitrary[SampleClassB] { for ( a1 <- arbA.arbitrary; a2 <- arbA.arbitrary; y <- Arbitrary.arbString.arbitrary - ) yield B(a1, a2, y) + ) yield SampleClassB(a1, a2, y) } - implicit def arbC: Arbitrary[C] = Arbitrary[C] { + implicit def arbC: Arbitrary[SampleClassC] = Arbitrary[SampleClassC] { for ( a <- arbA.arbitrary; b <- arbB.arbitrary; c <- arbA.arbitrary; d <- arbB.arbitrary; e <- arbB.arbitrary - ) yield C(a, b, c, d, e) + ) yield SampleClassC(a, b, c, d, e) } } trait CaseClassToTuplePropTests extends MacroPropTests { def shouldRoundTrip[A, B <: Product](t: A)(implicit proof: IsCaseClass[A], bij: Bijection[A, B]) { - val mgbij = isMg(bij) - t shouldBe mgbij.invert(mgbij(t)) + bij shouldBe a[MacroGenerated] + t shouldBe bij.invert(bij(t)) } def shouldRoundTrip[A, B <: Product](t: B)(implicit proof: IsCaseClass[A], bij: Bijection[A, B]) { - val mgbij = isMg(bij) - t shouldBe mgbij(mgbij.invert(t)) + bij shouldBe a[MacroGenerated] + t shouldBe bij(bij.invert(t)) } } class CaseClassToTupleRecurisvelyAppliedPropTests extends CaseClassToTuplePropTests { import MacroImplicits._ - import CommonMacroImplicits._ + import MacroImplicits._ import MacroCaseClasses._ property("case class A(Int, String) should round trip") { - forAll { v: A => shouldRoundTrip[A, Atup](v) } + forAll { v: SampleClassA => shouldRoundTrip[SampleClassA, Atup](v) } } property("case class B(A, A, String) should round trip") { - forAll { v: B => shouldRoundTrip[B, Btup](v) } + forAll { v: SampleClassB => shouldRoundTrip[SampleClassB, Btup](v) } } property("case class C(A, B, A, B, B) should round trip") { - forAll { v: C => shouldRoundTrip[C, Ctup](v) } + forAll { v: SampleClassC => shouldRoundTrip[SampleClassC, Ctup](v) } } property("case class A(Int, String) should round trip in reverse") { - forAll { v: Atup => shouldRoundTrip[A, Atup](v) } + forAll { v: Atup => shouldRoundTrip[SampleClassA, Atup](v) } } property("case class B(A, A, String) should round trip in reverse") { - forAll { v: Btup => shouldRoundTrip[B, Btup](v) } + forAll { v: Btup => shouldRoundTrip[SampleClassB, Btup](v) } } property("case class C(A, B, A, B, B) should round trip in reverse") { - forAll { v: Ctup => shouldRoundTrip[C, Ctup](v) } + forAll { v: Ctup => shouldRoundTrip[SampleClassC, Ctup](v) } } } class CaseClassToTupleNonRecursivelyAppliedPropTests extends CaseClassToTuplePropTests { - import LowerPriorityMacroImplicits._ - import CommonMacroImplicits._ + import MacroImplicits._ import MacroCaseClasses._ property("case class A(Int, String) should round trip") { - forAll { v: A => shouldRoundTrip[A, Atupnr](v) } + forAll { v: SampleClassA => shouldRoundTrip[SampleClassA, Atupnr](v) } } property("case class B(A, A, String) should round trip") { - forAll { v: B => shouldRoundTrip[B, Btupnr](v) } + forAll { v: SampleClassB => shouldRoundTrip[SampleClassB, Btupnr](v) } } property("case class C(A, B, A, B, B) should round trip") { - forAll { v: C => shouldRoundTrip[C, Ctupnr](v) } + forAll { v: SampleClassC => shouldRoundTrip[SampleClassC, Ctupnr](v) } } property("case class A(Int, String) should round trip in reverse") { - forAll { v: Atupnr => shouldRoundTrip[A, Atupnr](v) } + forAll { v: Atupnr => shouldRoundTrip[SampleClassA, Atupnr](v) } } property("case class B(A, A, String) should round trip in reverse") { - forAll { v: Btupnr => shouldRoundTrip[B, Btupnr](v) } + forAll { v: Btupnr => shouldRoundTrip[SampleClassB, Btupnr](v) } } property("case class C(A, B, A, B, B) should round trip in reverse") { - forAll { v: Ctupnr => shouldRoundTrip[C, Ctupnr](v) } + forAll { v: Ctupnr => shouldRoundTrip[SampleClassC, Ctupnr](v) } } } trait CaseClassToMapPropTests extends MacroPropTests { def shouldRoundTrip[A](t: A)(implicit proof: IsCaseClass[A], inj: Injection[A, Map[String, Any]]) { - val mginj = isMg(inj) - Success(t) shouldBe mginj.invert(mginj(t)) + inj shouldBe a[MacroGenerated] + Success(t) shouldEqual inj.invert(inj(t)) } def shouldRoundTrip[A](t: Map[String, Any])(implicit proof: IsCaseClass[A], inj: Injection[A, Map[String, Any]]) { - val mginj = isMg(inj) - Success(t) shouldBe mginj.invert(t).map { mginj(_) } + inj shouldBe a[MacroGenerated] + inj.invert(t).get + Success(t) shouldEqual inj.invert(t).map { inj(_) } } } class CaseClassToMapRecursivelyAppliedPropTests extends CaseClassToMapPropTests { import MacroImplicits._ - import CommonMacroImplicits._ + import MacroImplicits._ import MacroCaseClasses._ property("case class A(Int, String) should round trip") { - forAll { v: A => shouldRoundTrip[A](v) } + forAll { v: SampleClassA => shouldRoundTrip[SampleClassA](v) } } property("case class B(A, A, String) should round trip") { - forAll { v: B => shouldRoundTrip[B](v) } + forAll { v: SampleClassB => shouldRoundTrip[SampleClassB](v) } } property("case class C(A, B, A, B, B) should round trip") { - forAll { v: C => shouldRoundTrip[C](v) } + forAll { v: SampleClassC => shouldRoundTrip[SampleClassC](v) } } property("case class A(Int, String) should round trip in reverse") { - forAll { v: Atup => shouldRoundTrip[A](Map[String, Any]("x" -> v._1, "y" -> v._2)) } + forAll { v: Atup => shouldRoundTrip[SampleClassA](Map[String, Any]("x" -> v._1, "y" -> v._2)) } } property("case class B(A, A, String) should round trip in reverse") { forAll { v: Btup => - shouldRoundTrip[B]( + shouldRoundTrip[SampleClassB]( Map[String, Any]( "a1" -> Map[String, Any]("x" -> v._1._1, "y" -> v._1._2), "a2" -> Map[String, Any]("x" -> v._2._1, "y" -> v._2._2), @@ -160,7 +158,7 @@ class CaseClassToMapRecursivelyAppliedPropTests extends CaseClassToMapPropTests property("case class C(A, B, A, B, B) should round trip in reverse") { forAll { v: Ctup => - shouldRoundTrip[C]( + shouldRoundTrip[SampleClassC]( Map[String, Any]( "a" -> Map[String, Any]("x" -> v._1._1, "y" -> v._1._2), "b" -> Map[String, Any]( @@ -181,39 +179,38 @@ class CaseClassToMapRecursivelyAppliedPropTests extends CaseClassToMapPropTests } class CaseClassToMapNonRecursivelyAppliedPropTests extends CaseClassToMapPropTests { - import LowerPriorityMacroImplicits._ - import CommonMacroImplicits._ + import MacroImplicits._ import MacroCaseClasses._ property("case class A(Int, String) should round trip") { - forAll { v: A => shouldRoundTrip[A](v) } + forAll { v: SampleClassA => shouldRoundTrip[SampleClassA](v) } } property("case class B(A, A, String) should round trip") { - forAll { v: B => shouldRoundTrip[B](v) } + forAll { v: SampleClassB => shouldRoundTrip[SampleClassB](v) } } property("case class C(A, B, A, B, B) should round trip") { - forAll { v: C => shouldRoundTrip[C](v) } + forAll { v: SampleClassC => shouldRoundTrip[SampleClassC](v) } } property("case class A(Int, String) should round trip in reverse") { - forAll { v: Atupnr => shouldRoundTrip[A](Map[String, Any]("x" -> v._1, "y" -> v._2)) } + forAll { v: Atupnr => shouldRoundTrip[SampleClassA](Map[String, Any]("x" -> v._1, "y" -> v._2))(implicitly, Macros.caseClassToMap[SampleClassA](false)) } } property("case class B(A, A, String) should round trip in reverse") { forAll { v: Btupnr => - shouldRoundTrip[B]( + shouldRoundTrip[SampleClassB]( Map[String, Any]( "a1" -> v._1, "a2" -> v._2, - "y" -> v._3)) + "y" -> v._3))(implicitly, Macros.caseClassToMap[SampleClassB](false)) } } property("case class C(A, B, A, B, B) should round trip in reverse") { forAll { v: Ctupnr => - shouldRoundTrip[C](Map[String, Any]("a" -> v._1, "b" -> v._2, "c" -> v._3, "d" -> v._4, "e" -> v._5)) + shouldRoundTrip[SampleClassC](Map[String, Any]("a" -> v._1, "b" -> v._2, "c" -> v._3, "d" -> v._4, "e" -> v._5))(implicitly, Macros.caseClassToMap[SampleClassC](false)) } } } diff --git a/bijection-macros/src/test/scala/com/twitter/bijection/macros/MacroUnitTests.scala b/bijection-macros/src/test/scala/com/twitter/bijection/macros/MacroUnitTests.scala index ac5a164eb..607ebc88d 100644 --- a/bijection-macros/src/test/scala/com/twitter/bijection/macros/MacroUnitTests.scala +++ b/bijection-macros/src/test/scala/com/twitter/bijection/macros/MacroUnitTests.scala @@ -4,68 +4,47 @@ import org.scalatest.{ Matchers, WordSpec } import com.twitter.bijection._ import com.twitter.bijection.macros._ -import com.twitter.bijection.macros.common.{ MacroImplicits => CommonMacroImplicits, _ } import com.twitter.chill.Externalizer -object MacroCaseClasses extends java.io.Serializable { - type Atup = (Int, String) - type Btup = (Atup, Atup, String) - type Ctup = (Atup, Btup, Atup, Btup, Btup) - - type Atupnr = (Int, String) - type Btupnr = (A, A, String) - type Ctupnr = (A, B, A, B, B) - - case class A(x: Int, y: String) - case class B(a1: A, a2: A, y: String) - case class C(a: A, b: B, c: A, d: B, e: B) -} - -trait MacroTestHelper extends Matchers { - def isMg[T](t: T): T = { - t shouldBe a[MacroGenerated] - t - } - - def canExternalize(t: AnyRef) { Externalizer(t).javaWorks shouldBe true } -} - class MacroUnitTests extends WordSpec with Matchers with MacroTestHelper { - import CommonMacroImplicits._ + import MacroImplicits._ import MacroCaseClasses._ - def doesJavaWork[A, B](implicit bij: Bijection[A, B]) { canExternalize(isMg(bij)) } - def doesJavaWork[A](implicit bij: Injection[A, Map[String, Any]]) { canExternalize(isMg(bij)) } + def doesJavaWork[A, B](implicit bij: Bijection[A, B]) { + bij shouldBe a[MacroGenerated] + canExternalize(bij) + } + def doesJavaWork[A](implicit bij: Injection[A, Map[String, Any]]) { + bij shouldBe a[MacroGenerated] + canExternalize(bij) + } "Recursively applied" when { - import MacroImplicits._ "MacroGenerated Bijection to tuple" should { - "be serializable for case class A" in { doesJavaWork[A, Atup] } - "be serializable for case class B" in { doesJavaWork[B, Btup] } - "be serializable for case class C" in { doesJavaWork[C, Ctup] } + "be serializable for case class A" in { doesJavaWork[SampleClassA, Atup] } + "be serializable for case class B" in { doesJavaWork[SampleClassB, Btup] } + "be serializable for case class C" in { doesJavaWork[SampleClassC, Ctup] } } "MacroGenerated Injection to map" should { - "be serializable for case class A" in { doesJavaWork[A] } - "be serializable for case class B" in { doesJavaWork[B] } - "be serializable for case class C" in { doesJavaWork[C] } + "be serializable for case class A" in { doesJavaWork[SampleClassA] } + "be serializable for case class B" in { doesJavaWork[SampleClassB] } + "be serializable for case class C" in { doesJavaWork[SampleClassC] } } } "Non-recursively applied" when { - import LowerPriorityMacroImplicits._ - "MacroGenerated Bijection to tuple" should { - "be serializable for case class A" in { doesJavaWork[A, Atupnr] } - "be serializable for case class B" in { doesJavaWork[B, Btupnr] } - "be serializable for case class C" in { doesJavaWork[C, Ctupnr] } + "be serializable for case class A" in { doesJavaWork[SampleClassA, Atupnr] } + "be serializable for case class B" in { doesJavaWork[SampleClassB, Btupnr] } + "be serializable for case class C" in { doesJavaWork[SampleClassC, Ctupnr] } } "MacroGenerated Injection to map" should { - "be serializable for case class A" in { doesJavaWork[A] } - "be serializable for case class B" in { doesJavaWork[B] } - "be serializable for case class C" in { doesJavaWork[C] } + "be serializable for case class A" in { doesJavaWork[SampleClassA] } + "be serializable for case class B" in { doesJavaWork[SampleClassB] } + "be serializable for case class C" in { doesJavaWork[SampleClassC] } } } } diff --git a/bijection-macros/src/test/scala/com/twitter/bijection/macros/TestHelpers.scala b/bijection-macros/src/test/scala/com/twitter/bijection/macros/TestHelpers.scala new file mode 100644 index 000000000..c671f4baa --- /dev/null +++ b/bijection-macros/src/test/scala/com/twitter/bijection/macros/TestHelpers.scala @@ -0,0 +1,25 @@ +package com.twitter.bijection.macros + +import org.scalatest.Matchers +import com.twitter.chill.Externalizer + +object MacroCaseClasses extends java.io.Serializable { + type Atup = (Int, String) + type Btup = (Atup, Atup, String) + type Ctup = (Atup, Btup, Atup, Btup, Btup) + + // These are s single level unpacking into tuples + // of the case classes below + type Atupnr = (Int, String) + type Btupnr = (SampleClassA, SampleClassA, String) + type Ctupnr = (SampleClassA, SampleClassB, SampleClassA, SampleClassB, SampleClassB) + + case class SampleClassA(x: Int, y: String) + case class SampleClassB(a1: SampleClassA, a2: SampleClassA, y: String) + case class SampleClassC(a: SampleClassA, b: SampleClassB, c: SampleClassA, d: SampleClassB, e: SampleClassB) + class SampleClassD // Non-case class +} + +trait MacroTestHelper extends Matchers { + def canExternalize(t: AnyRef) { Externalizer(t).javaWorks shouldBe true } +} diff --git a/project/Build.scala b/project/Build.scala index ed53d85d6..6f577cc42 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -149,7 +149,6 @@ object BijectionBuild extends Build { bijectionHbase, bijectionJodaTime, bijectionJson4s, - bijectionMacrosCommon, bijectionMacros ) @@ -280,17 +279,6 @@ object BijectionBuild extends Build { ) ).dependsOn(bijectionCore % "test->test;compile->compile") - lazy val bijectionMacrosCommon = module("macros-common").settings( - libraryDependencies <++= (scalaVersion) { scalaVersion => Seq( - "org.scala-lang" % "scala-library" % scalaVersion, - "org.scala-lang" % "scala-reflect" % scalaVersion, - "org.scalatest" %% "scalatest" % "2.2.2", - "com.twitter" %% "chill" % "0.5.0" % "test" - ) ++ (if (scalaVersion.startsWith("2.10")) Seq("org.scalamacros" %% "quasiquotes" % "2.0.1") else Seq()) - }, - addCompilerPlugin("org.scalamacros" % "paradise" % "2.0.1" cross CrossVersion.full) - ).dependsOn(bijectionCore) - lazy val bijectionMacros = module("macros").settings( libraryDependencies <++= (scalaVersion) { scalaVersion => Seq( "org.scala-lang" % "scala-library" % scalaVersion, @@ -300,5 +288,5 @@ object BijectionBuild extends Build { ) ++ (if (scalaVersion.startsWith("2.10")) Seq("org.scalamacros" %% "quasiquotes" % "2.0.1") else Seq()) }, addCompilerPlugin("org.scalamacros" % "paradise" % "2.0.1" cross CrossVersion.full) - ).dependsOn(bijectionCore, bijectionMacrosCommon) + ).dependsOn(bijectionCore) }