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 new file mode 100644 index 000000000..dc3a1678d --- /dev/null +++ b/bijection-macros-common/src/main/scala/com/twitter/bijection/macros/common/MacroImplicits.scala @@ -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] +} diff --git a/bijection-macros-common/src/main/scala/com/twitter/bijection/macros/common/Macros.scala b/bijection-macros-common/src/main/scala/com/twitter/bijection/macros/common/Macros.scala new file mode 100644 index 000000000..1db4dbcba --- /dev/null +++ b/bijection-macros-common/src/main/scala/com/twitter/bijection/macros/common/Macros.scala @@ -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 diff --git a/bijection-macros-common/src/main/scala/com/twitter/bijection/macros/common/impl/MacroImpl.scala b/bijection-macros-common/src/main/scala/com/twitter/bijection/macros/common/impl/MacroImpl.scala new file mode 100644 index 000000000..c1ffb5b32 --- /dev/null +++ b/bijection-macros-common/src/main/scala/com/twitter/bijection/macros/common/impl/MacroImpl.scala @@ -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 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 new file mode 100644 index 000000000..57bc9006d --- /dev/null +++ b/bijection-macros-common/src/test/scala/com/twitter/bijection/macros/common/MacroDepHygiene.scala @@ -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] + } + } +} 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 new file mode 100644 index 000000000..1865e6c67 --- /dev/null +++ b/bijection-macros/src/main/scala/com/twitter/bijection/macros/MacroImplicits.scala @@ -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] +} 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 new file mode 100644 index 000000000..7d3f4c957 --- /dev/null +++ b/bijection-macros/src/main/scala/com/twitter/bijection/macros/Macros.scala @@ -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] +} 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 new file mode 100644 index 000000000..edb6eb1b1 --- /dev/null +++ b/bijection-macros/src/main/scala/com/twitter/bijection/macros/impl/MacroImpl.scala @@ -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 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 new file mode 100644 index 000000000..2ec102082 --- /dev/null +++ b/bijection-macros/src/test/scala/com/twitter/bijection/macros/MacroPropTests.scala @@ -0,0 +1,219 @@ +package com.twitter.bijection.macros + +import scala.util.Success + +import org.scalacheck.Arbitrary +import org.scalatest.{ Matchers, PropSpec } +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] { + for ( + a <- Arbitrary.arbInt.arbitrary; + b <- Arbitrary.arbString.arbitrary + ) yield A(a, b) + } + + implicit def arbB: Arbitrary[B] = Arbitrary[B] { + for ( + a1 <- arbA.arbitrary; + a2 <- arbA.arbitrary; + y <- Arbitrary.arbString.arbitrary + ) yield B(a1, a2, y) + } + + implicit def arbC: Arbitrary[C] = Arbitrary[C] { + for ( + a <- arbA.arbitrary; + b <- arbB.arbitrary; + c <- arbA.arbitrary; + d <- arbB.arbitrary; + e <- arbB.arbitrary + ) yield C(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)) + } + + 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)) + } +} + +class CaseClassToTupleRecurisvelyAppliedPropTests extends CaseClassToTuplePropTests { + import MacroImplicits._ + import CommonMacroImplicits._ + import MacroCaseClasses._ + + property("case class A(Int, String) should round trip") { + forAll { v: A => shouldRoundTrip[A, Atup](v) } + } + + property("case class B(A, A, String) should round trip") { + forAll { v: B => shouldRoundTrip[B, Btup](v) } + } + + property("case class C(A, B, A, B, B) should round trip") { + forAll { v: C => shouldRoundTrip[C, Ctup](v) } + } + + property("case class A(Int, String) should round trip in reverse") { + forAll { v: Atup => shouldRoundTrip[A, Atup](v) } + } + + property("case class B(A, A, String) should round trip in reverse") { + forAll { v: Btup => shouldRoundTrip[B, Btup](v) } + } + + property("case class C(A, B, A, B, B) should round trip in reverse") { + forAll { v: Ctup => shouldRoundTrip[C, Ctup](v) } + } +} + +class CaseClassToTupleNonRecursivelyAppliedPropTests extends CaseClassToTuplePropTests { + import LowerPriorityMacroImplicits._ + import CommonMacroImplicits._ + import MacroCaseClasses._ + + property("case class A(Int, String) should round trip") { + forAll { v: A => shouldRoundTrip[A, Atupnr](v) } + } + + property("case class B(A, A, String) should round trip") { + forAll { v: B => shouldRoundTrip[B, Btupnr](v) } + } + + property("case class C(A, B, A, B, B) should round trip") { + forAll { v: C => shouldRoundTrip[C, Ctupnr](v) } + } + + property("case class A(Int, String) should round trip in reverse") { + forAll { v: Atupnr => shouldRoundTrip[A, Atupnr](v) } + } + + property("case class B(A, A, String) should round trip in reverse") { + forAll { v: Btupnr => shouldRoundTrip[B, Btupnr](v) } + } + + property("case class C(A, B, A, B, B) should round trip in reverse") { + forAll { v: Ctupnr => shouldRoundTrip[C, 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)) + } + + 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(_) } + } +} + +class CaseClassToMapRecursivelyAppliedPropTests extends CaseClassToMapPropTests { + import MacroImplicits._ + import CommonMacroImplicits._ + import MacroCaseClasses._ + + property("case class A(Int, String) should round trip") { + forAll { v: A => shouldRoundTrip[A](v) } + } + + property("case class B(A, A, String) should round trip") { + forAll { v: B => shouldRoundTrip[B](v) } + } + + property("case class C(A, B, A, B, B) should round trip") { + forAll { v: C => shouldRoundTrip[C](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)) } + } + + property("case class B(A, A, String) should round trip in reverse") { + forAll { v: Btup => + shouldRoundTrip[B]( + 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), + "y" -> v._3)) + } + } + + property("case class C(A, B, A, B, B) should round trip in reverse") { + forAll { v: Ctup => + shouldRoundTrip[C]( + Map[String, Any]( + "a" -> Map[String, Any]("x" -> v._1._1, "y" -> v._1._2), + "b" -> Map[String, Any]( + "a1" -> Map[String, Any]("x" -> v._2._1._1, "y" -> v._2._1._2), + "a2" -> Map[String, Any]("x" -> v._2._2._1, "y" -> v._2._2._2), + "y" -> v._2._3), + "c" -> Map[String, Any]("x" -> v._3._1, "y" -> v._3._2), + "d" -> Map[String, Any]( + "a1" -> Map[String, Any]("x" -> v._4._1._1, "y" -> v._4._1._2), + "a2" -> Map[String, Any]("x" -> v._4._2._1, "y" -> v._4._2._2), + "y" -> v._4._3), + "e" -> Map[String, Any]( + "a1" -> Map[String, Any]("x" -> v._5._1._1, "y" -> v._5._1._2), + "a2" -> Map[String, Any]("x" -> v._5._2._1, "y" -> v._5._2._2), + "y" -> v._5._3))) + } + } +} + +class CaseClassToMapNonRecursivelyAppliedPropTests extends CaseClassToMapPropTests { + import LowerPriorityMacroImplicits._ + import CommonMacroImplicits._ + import MacroCaseClasses._ + + property("case class A(Int, String) should round trip") { + forAll { v: A => shouldRoundTrip[A](v) } + } + + property("case class B(A, A, String) should round trip") { + forAll { v: B => shouldRoundTrip[B](v) } + } + + property("case class C(A, B, A, B, B) should round trip") { + forAll { v: C => shouldRoundTrip[C](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)) } + } + + property("case class B(A, A, String) should round trip in reverse") { + forAll { v: Btupnr => + shouldRoundTrip[B]( + Map[String, Any]( + "a1" -> v._1, + "a2" -> v._2, + "y" -> v._3)) + } + } + + 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)) + } + } +} 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 new file mode 100644 index 000000000..ac5a164eb --- /dev/null +++ b/bijection-macros/src/test/scala/com/twitter/bijection/macros/MacroUnitTests.scala @@ -0,0 +1,71 @@ +package com.twitter.bijection.macros + +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 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)) } + + "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] } + } + + "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] } + } + } + + "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] } + } + + "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] } + } + } +} diff --git a/project/Build.scala b/project/Build.scala index d2ebea110..ed53d85d6 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -148,7 +148,9 @@ object BijectionBuild extends Build { bijectionAvro, bijectionHbase, bijectionJodaTime, - bijectionJson4s + bijectionJson4s, + bijectionMacrosCommon, + bijectionMacros ) def module(name: String) = { @@ -278,5 +280,25 @@ 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, + "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, bijectionMacrosCommon) }