From 6431fba02b1663701564adacef07a292ff7c32d7 Mon Sep 17 00:00:00 2001 From: Jon Pretty Date: Tue, 9 Jan 2018 10:02:39 +0000 Subject: Make access of combine and dispatch lazy --- core/shared/src/main/scala/magnolia.scala | 38 +++++++++++++--------------- examples/shared/src/main/scala/default.scala | 16 ++++++++++++ tests/src/main/scala/tests.scala | 10 ++++++++ 3 files changed, 43 insertions(+), 21 deletions(-) diff --git a/core/shared/src/main/scala/magnolia.scala b/core/shared/src/main/scala/magnolia.scala index 759deb6..c0fc10d 100644 --- a/core/shared/src/main/scala/magnolia.scala +++ b/core/shared/src/main/scala/magnolia.scala @@ -96,30 +96,26 @@ object Magnolia { } } - val typeclassConstructor = getTypeMember("Typeclass") - - def checkMethod(termName: String, category: String, expected: String): Unit = { + def getMethod(termName: String)(fn: MethodSymbol => Tree): Option[Tree] = { val term = TermName(termName) - val combineClass = prefixType.baseClasses - .find { cls => - cls.asType.toType.decl(term) != NoSymbol - } - .getOrElse { - fail(s"the method `$termName` must be defined on the derivation $prefixObject to derive typeclasses for $category") - } - val firstParamBlock = combineClass.asType.toType.decl(term).asTerm.asMethod.paramLists.head - if (firstParamBlock.lengthCompare(1) != 0) - fail(s"the method `combine` should take a single parameter of type $expected") + val cls = prefixType.baseClasses.find(_.asType.toType.decl(term) != NoSymbol) + cls.map(_.asType.toType.decl(term).asTerm.asMethod).map(fn) } - // FIXME: Only run these methods if they're used, particularly `dispatch` - checkMethod("combine", "case classes", "CaseClass[Typeclass, _]") - checkMethod("dispatch", "sealed traits", "SealedTrait[Typeclass, _]") + val typeclassConstructor = getTypeMember("Typeclass") + + lazy val combineMethod = getMethod("combine") { m => q"${c.prefix}.combine" }.getOrElse { + fail("the method `combine` should be defined on the derivation object") + } + + lazy val dispatchMethod = getMethod("dispatch") { m => q"${c.prefix}.dispatch" }.getOrElse { + fail("the method `dispatch` should be defined on the derivation object") + } val removeDeferred = new Transformer { override def transform(tree: Tree) = tree match { - case q"$magnolia.Deferred.apply[$_](${Literal(Constant(method: String))})" - if magnolia.symbol == magnoliaPkg => + case q"$pkg.Deferred.apply[$_](${Literal(Constant(method: String))})" + if pkg.symbol == magnoliaPkg => q"${TermName(method)}" case _ => super.transform(tree) @@ -183,7 +179,7 @@ object Magnolia { val result = if (isCaseObject) { val impl = q""" $typeNameDef - ${c.prefix}.combine($magnoliaPkg.Magnolia.caseClass[$typeConstructor, $genericType]( + $combineMethod($magnoliaPkg.Magnolia.caseClass[$typeConstructor, $genericType]( $typeName, true, false, new $scalaPkg.Array(0), _ => ${genericType.typeSymbol.asClass.module}) ) """ @@ -277,7 +273,7 @@ object Magnolia { $typeNameDef - ${c.prefix}.combine($magnoliaPkg.Magnolia.caseClass[$typeConstructor, $genericType]( + $combineMethod($magnoliaPkg.Magnolia.caseClass[$typeConstructor, $genericType]( $typeName, false, $isValueClass, @@ -342,7 +338,7 @@ object Magnolia { $typeNameDef - ${c.prefix}.dispatch(new $magnoliaPkg.SealedTrait( + $dispatchMethod(new $magnoliaPkg.SealedTrait( $typeName, $subtypesVal: $scalaPkg.Array[$magnoliaPkg.Subtype[$typeConstructor, $genericType]]) ): $resultType diff --git a/examples/shared/src/main/scala/default.scala b/examples/shared/src/main/scala/default.scala index 4b781a5..fb5966f 100644 --- a/examples/shared/src/main/scala/default.scala +++ b/examples/shared/src/main/scala/default.scala @@ -36,3 +36,19 @@ object Default { /** generates default instances of [[Default]] for case classes and sealed traits */ implicit def gen[T]: Default[T] = macro Magnolia.gen[T] } + +object DefaultNoCoproduct { + + type Typeclass[T] = Default[T] + + /** constructs a default for each parameter, using the constructor default (if provided), + * otherwise using a typeclass-provided default */ + def combine[T](ctx: CaseClass[Default, T]): Default[T] = new Default[T] { + def default = ctx.construct { param => + param.default.getOrElse(param.typeclass.default) + } + } + + /** generates default instances of [[Default]] for case classes and sealed traits */ + implicit def gen[T]: Default[T] = macro Magnolia.gen[T] +} diff --git a/tests/src/main/scala/tests.scala b/tests/src/main/scala/tests.scala index f56afcb..909ee82 100644 --- a/tests/src/main/scala/tests.scala +++ b/tests/src/main/scala/tests.scala @@ -28,6 +28,7 @@ class Length(val value: Int) extends AnyVal case class FruitBasket(fruits: Fruit*) case class Lunchbox(fruit: Fruit, drink: String) + object Fruit { implicit val showFruit: Show[String, Fruit] = new Show[String, Fruit] { def show(f: Fruit): String = f.name } @@ -45,6 +46,7 @@ case class `%%`(`/`: Int, `#`: String) case class Param(a: String, b: String) case class Test(param: Param) + object Test { def apply(): Test = Test(Param("", "")) @@ -355,5 +357,13 @@ object Tests extends TestApp { Show.gen[Path[String]].show(OffRoad(Some(Crossroad(Destination("A"), Destination("B"))))) }.assert(_ == "OffRoad(path=Crossroad(left=Destination(value=A),right=Destination(value=B)))") + + test("does not derive for coproduct without dispatch defined") { + scalac"DefaultNoCoproduct.gen[Entity]" + }.assert(_ == TypecheckError("magnolia: the method `dispatch` should be defined on the derivation object")) + + test("still derives for case class without dispatch defined") { + DefaultNoCoproduct.gen[Company].default + }.assert(_ == Company("")) } } -- cgit v1.2.3