Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add possibility to separate gen from combine #279

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/core/interface.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
21 changes: 14 additions & 7 deletions src/core/magnolia.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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]]
Expand All @@ -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

Expand Down Expand Up @@ -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"))

Expand Down Expand Up @@ -380,7 +387,7 @@ object Magnolia {

val impl = q"""
$typeNameDef
${c.prefix}.combine(new $caseClassType(
$prefix.combine(new $caseClassType(
$typeName,
true,
false,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -605,20 +612,20 @@ 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),
$ArrayObj(..$classTypeAnnotationTrees)
))
}""")
} 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

Expand Down
86 changes: 86 additions & 0 deletions src/examples/proxies.scala
Original file line number Diff line number Diff line change
@@ -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
}
}
36 changes: 36 additions & 0 deletions src/test/tests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}