diff --git a/src/core/interface.scala b/src/core/interface.scala index 5c93fbe3..e61861f5 100644 --- a/src/core/interface.scala +++ b/src/core/interface.scala @@ -440,6 +440,13 @@ final case class TypeName(owner: String, short: String, typeArguments: Seq[TypeN */ final class debug(typeNamePart: String = "") extends scala.annotation.StaticAnnotation +/** + * Allows `combine`, `dispatch` and `fallback` to be implemented in another object. + * This makes it possible to export more than one derivation in an object + * @param companion the object where the methods are defined + */ +final class proxy(companion: Any) extends scala.annotation.StaticAnnotation + private[magnolia] final case class EarlyExit[E](e: E) extends Exception with util.control.NoStackTrace object MagnoliaUtil { diff --git a/src/core/magnolia.scala b/src/core/magnolia.scala index 5fdb1980..e44cd562 100644 --- a/src/core/magnolia.scala +++ b/src/core/magnolia.scala @@ -106,6 +106,7 @@ object Magnolia { val NoneObj = reify(None).tree val ParamObj = reify(Param).tree val ParamSym = symbolOf[Param[Any, Any]] + val ProxyTpe = typeOf[proxy] val ReadOnlyCaseClassSym = symbolOf[ReadOnlyCaseClass[Any, Any]] val ReadOnlyParamObj = reify(ReadOnlyParam).tree val ReadOnlyParamSym = symbolOf[ReadOnlyParam[Any, Any]] @@ -118,7 +119,13 @@ object Magnolia { val TypeClassNme = TypeName("Typeclass") val TypeNameObj = reify(magnolia.TypeName).tree - val prefixType = c.prefix.tree.tpe + val proxy: Option[c.Expr[_]] = + c.macroApplication.symbol.annotations.collectFirst { + case a if a.tree.tpe <:< ProxyTpe => c.Expr(a.tree.children(1)) + } + + val prefix: Expr[_] = proxy.getOrElse(c.prefix) + val prefixType = prefix.tree.tpe val prefixObject = prefixType.typeSymbol val prefixName = prefixObject.name.decodedName @@ -234,7 +241,7 @@ object Magnolia { def extractParameterBlockFor(termName: String, category: String): List[Symbol] = { val term = TermName(termName) - val classWithTerm = c.prefix.tree.tpe.baseClasses + val classWithTerm = prefix.tree.tpe.baseClasses .find(cls => cls.asType.toType.decl(term) != NoSymbol) .getOrElse(error(s"the method `$termName` must be defined on the derivation $prefixObject to derive typeclasses for $category")) @@ -380,7 +387,7 @@ object Magnolia { val impl = q""" $typeNameDef - ${c.prefix}.combine(new $caseClassType( + $prefix.combine(new $caseClassType( $typeName, true, false, @@ -549,7 +556,7 @@ object Magnolia { val $paramsVal = new $ArrayClass[$paramType](${assignments.length}) ..$assignments $typeNameDef - ${c.prefix}.combine(new $caseClassType( + $prefix.combine(new $caseClassType( $typeName, false, $isValueClass, @@ -605,7 +612,7 @@ object Magnolia { val $subtypesVal = new $ArrayClass[$subType](${assignments.size}) ..$assignments $typeNameDef - ${c.prefix}.dispatch(new $SealedTraitSym( + $prefix.dispatch(new $SealedTraitSym( $typeName, $subtypesVal: $ArrayClass[$subType], $ArrayObj(..$classAnnotationTrees), @@ -613,12 +620,12 @@ object Magnolia { )) }""") } else if (!typeSymbol.isParameter) { - c.prefix.tree.tpe.baseClasses + prefix.tree.tpe.baseClasses .find { cls => cls.asType.toType.decl(TermName("fallback")) != NoSymbol }.map { _ => warning(s"using fallback derivation for $genericType") - q"""${c.prefix}.fallback[$genericType]""" + q"""$prefix.fallback[$genericType]""" } } else None diff --git a/src/examples/proxies.scala b/src/examples/proxies.scala new file mode 100644 index 00000000..16449541 --- /dev/null +++ b/src/examples/proxies.scala @@ -0,0 +1,86 @@ +/* + + Magnolia, version 0.17.0. Copyright 2018-20 Jon Pretty, Propensive OÜ. + + The primary distribution site is: https://propensive.com/ + + Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + compliance with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software distributed under the License is + distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and limitations under the License. + +*/ +package magnolia.examples + +import magnolia._ +import scala.language.experimental.macros + +/** Demonstrate some flexibility for the user-facing API of your type classes + * + * - how to export multiple type class derivations behind one underscore import + * - how to offer both auto and semiauto derivations through different imports + */ +object proxies { + // if wanted these can go in a trait and be reexported through other paths as well + object auto extends auto + + trait auto { + @proxy(ToString1) + implicit def deriveToString1[A]: ToString1[A] = macro Magnolia.gen[A] + + @proxy(ToString2) + implicit def deriveToString2[A]: ToString2[A] = macro Magnolia.gen[A] + } + + // or directly in an object, or in the companion objects or wherever + object semiauto { + @proxy(ToString1) + def deriveToString1[A]: ToString1[A] = macro Magnolia.gen[A] + + @proxy(ToString2) + def deriveToString2[A]: ToString2[A] = macro Magnolia.gen[A] + } + + implicit class Syntax[T](private val t: T) extends AnyVal { + def str1(implicit ev: ToString1[T]): String = ev.str1(t) + def str2(implicit ev: ToString2[T]): String = ev.str2(t) + } + + // an example type class which just stringifies data in a non-useful way + trait ToString1[A] { + def str1(a: A): String + } + + object ToString1 { + def apply[A: ToString1]: ToString1[A] = implicitly + + type Typeclass[A] = ToString1[A] + + def combine[A](ctx: ReadOnlyCaseClass[ToString1, A]): ToString1[A] = + (a: A) => ctx.parameters.map(param => param.typeclass.str1(param.dereference(a))).mkString(", ") + + implicit val str: ToString1[String] = (a: String) => a + implicit val int: ToString1[Int] = (a: Int) => a.toString + } + + // this is just here to test that we can export more than one derivation + trait ToString2[A] { + def str2(a: A): String + } + + object ToString2 { + def apply[A: ToString2]: ToString2[A] = implicitly + + type Typeclass[A] = ToString2[A] + + def combine[A](ctx: ReadOnlyCaseClass[ToString2, A]): ToString2[A] = + (a: A) => ctx.parameters.map(param => param.typeclass.str2(param.dereference(a))).mkString("; ") + + implicit val str: ToString2[String] = (a: String) => a + implicit val int: ToString2[Int] = (a: Int) => a.toString + } +} diff --git a/src/test/tests.scala b/src/test/tests.scala index 3a5ed25c..1a156881 100644 --- a/src/test/tests.scala +++ b/src/test/tests.scala @@ -734,5 +734,41 @@ object Tests extends Suite("Magnolia tests") { test("support dispatch without combine") { implicitly[NoCombine[Halfy]].nameOf(Righty()) }.assert(_ == "Righty") + + test("proxies: semiauto ToString1") { + import proxies._, semiauto._ + implicit val x: ToString1[Param] = deriveToString1[Param] + Param("a", "b").str1 + }.assert(_ == "a, b") + + test("proxies: semiauto ToString1 is not auto") { + import proxies._, semiauto._ + scalac"""Param("a", "b").str1""" + }.assert(_ == TypecheckError(txt"could not find implicit value for parameter ev: magnolia.examples.proxies.ToString1[magnolia.tests.Param]")) + + test("proxies: semiauto ToString1 (nested)") { + import proxies._, semiauto._ + implicit val x: ToString1[Param] = deriveToString1[Param] + implicit val y: ToString1[Test] = deriveToString1[Test] + Test(Param("a", "b")).str1 + }.assert(_ == "a, b") + + test("proxies: semiauto ToString1 is not auto (nested)") { + import proxies._, semiauto._ + // skip `deriveToString1[Param]` + scalac"deriveToString1[Test]" + }.assert(_ == TypecheckError(txt"""magnolia: could not find ToString1.Typeclass for type magnolia.tests.Param + in parameter 'param' of product type magnolia.tests.Test +""")) + + test("proxies: auto ToString1 (nested)") { + import proxies._, auto._ + Test(Param("a", "b")).str1 + }.assert(_ == "a, b") + + test("proxies: auto ToString2 (nested)") { + import proxies._, auto._ + Test(Param("a", "b")).str2 + }.assert(_ == "a; b") } }