From e9b52248855f9252520a232b28a841ea382922a1 Mon Sep 17 00:00:00 2001 From: Georgi Krastev Date: Wed, 29 Nov 2017 12:36:04 +0100 Subject: Add support for repeated (vararg) parameters Added a `Param.repeated` flag to `interface`. --- CONTRIBUTORS | 2 +- core/shared/src/main/scala/interface.scala | 11 +++++++++++ core/shared/src/main/scala/magnolia.scala | 29 +++++++++++++++++++++------- examples/shared/src/main/scala/default.scala | 3 +++ examples/shared/src/main/scala/show.scala | 6 ++++++ tests/src/main/scala/tests.scala | 18 +++++++++++++++-- 6 files changed, 59 insertions(+), 10 deletions(-) diff --git a/CONTRIBUTORS b/CONTRIBUTORS index f495d8d..e376ca4 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -1,3 +1,3 @@ Jon Pretty [@propensive](https://twitter.com/propensive/) Loïc Descotte - +Georgi Krastev [@joro_kr](https://twitter.com/joro_kr/) diff --git a/core/shared/src/main/scala/interface.scala b/core/shared/src/main/scala/interface.scala index 02c1cc5..193a6f9 100644 --- a/core/shared/src/main/scala/interface.scala +++ b/core/shared/src/main/scala/interface.scala @@ -46,6 +46,17 @@ trait Param[Typeclass[_], Type] { /** the name of the parameter */ def label: String + /** flag indicating a repeated (aka. vararg) parameter + * + * For example, for a case class, + *
+    * case class Account(id: String, emails: String*)
+    * 
+ * the [[Param]] instance corresponding to the `emails` parameter would be `repeated` and have a + * [[PType]] equal to the type `Seq[String]`. Note that only the last parameter of a case class + * can be repeated. */ + def repeated: Boolean + /** the typeclass instance associated with this parameter * * This is the instance of the type `Typeclass[PType]` which will have been discovered by diff --git a/core/shared/src/main/scala/magnolia.scala b/core/shared/src/main/scala/magnolia.scala index 2113dff..c4c13e6 100644 --- a/core/shared/src/main/scala/magnolia.scala +++ b/core/shared/src/main/scala/magnolia.scala @@ -69,6 +69,9 @@ object Magnolia { val magnoliaPkg = q"_root_.magnolia" val scalaPkg = q"_root_.scala" + val repeatedParamClass = definitions.RepeatedParamClass + val scalaSeqType = typeOf[Seq[_]].typeConstructor + val prefixType = c.prefix.tree.tpe def companionRef(tpe: Type): Tree = { @@ -239,6 +242,7 @@ object Magnolia { val className = genericType.typeSymbol.name.decodedName.toString case class CaseParam(sym: c.universe.MethodSymbol, + repeated: Boolean, typeclass: c.Tree, paramType: c.Type, ref: c.TermName) @@ -246,13 +250,21 @@ object Magnolia { val caseParamsReversed = caseClassParameters.foldLeft[List[CaseParam]](Nil) { (acc, param) => val paramName = param.name.decodedName.toString - val paramType = param.returnType.substituteTypes(genericType.etaExpand.typeParams, - genericType.typeArgs) + val paramTypeSubstituted = param.returnType.substituteTypes( + genericType.etaExpand.typeParams, + genericType.typeArgs) + + val (repeated, paramType) = paramTypeSubstituted match { + case TypeRef(_, `repeatedParamClass`, typeArgs) => + true -> appliedType(scalaSeqType, typeArgs) + case tpe => + false -> tpe + } val predefinedRef = acc.find(_.paramType == paramType) val caseParamOpt = predefinedRef.map { backRef => - CaseParam(param, q"()", paramType, backRef.ref) :: acc + CaseParam(param, repeated, q"()", paramType, backRef.ref) :: acc } caseParamOpt.getOrElse { @@ -265,7 +277,7 @@ object Magnolia { val ref = TermName(c.freshName("paramTypeclass")) val assigned = q"""val $ref = $derivedImplicit""" - CaseParam(param, assigned, paramType, ref) :: acc + CaseParam(param, repeated, assigned, paramType, ref) :: acc } } @@ -299,10 +311,10 @@ object Magnolia { } else List(q"$scalaPkg.None") val assignments = caseParams.zip(defaults).zipWithIndex.map { - case ((CaseParam(param, typeclass, paramType, ref), defaultVal), idx) => + case ((CaseParam(param, repeated, typeclass, paramType, ref), defaultVal), idx) => q"""$paramsVal($idx) = $magnoliaPkg.Magnolia.param[$typeConstructor, $genericType, $paramType]( - ${param.name.decodedName.toString}, $ref, $defaultVal, _.${param.name} + ${param.name.decodedName.toString}, $repeated, $ref, $defaultVal, _.${param.name} )""" } @@ -323,7 +335,8 @@ object Magnolia { ($fnVal: $magnoliaPkg.Param[$typeConstructor, $genericType] => Any) => new $genericType(..${caseParams.zipWithIndex.map { case (typeclass, idx) => - q"$fnVal($paramsVal($idx)).asInstanceOf[${typeclass.paramType}]" + val arg = q"$fnVal($paramsVal($idx)).asInstanceOf[${typeclass.paramType}]" + if (typeclass.repeated) q"$arg: _*" else arg }}) )) }""" @@ -456,11 +469,13 @@ object Magnolia { * This method is intended to be called only from code generated by the Magnolia macro, and * should not be called directly from users' code. */ def param[Tc[_], T, P](name: String, + isRepeated: Boolean, typeclassParam: Tc[P], defaultVal: => Option[P], deref: T => P) = new Param[Tc, T] { type PType = P def label: String = name + def repeated: Boolean = isRepeated def default: Option[PType] = defaultVal def typeclass: Tc[PType] = typeclassParam def dereference(t: T): PType = deref(t) diff --git a/examples/shared/src/main/scala/default.scala b/examples/shared/src/main/scala/default.scala index 4c1b634..4b781a5 100644 --- a/examples/shared/src/main/scala/default.scala +++ b/examples/shared/src/main/scala/default.scala @@ -30,6 +30,9 @@ object Default { /** default value for ints; 0 */ implicit val int: Default[Int] = new Default[Int] { def default = 0 } + /** default value for sequences; the empty sequence */ + implicit def seq[A]: Default[Seq[A]] = new Typeclass[Seq[A]] { def default = Seq.empty } + /** generates default instances of [[Default]] for case classes and sealed traits */ implicit def gen[T]: Default[T] = macro Magnolia.gen[T] } diff --git a/examples/shared/src/main/scala/show.scala b/examples/shared/src/main/scala/show.scala index 50b34ee..9f634ba 100644 --- a/examples/shared/src/main/scala/show.scala +++ b/examples/shared/src/main/scala/show.scala @@ -60,4 +60,10 @@ object Show extends GenericShow[String] { implicit val int: Show[String, Int] = new Show[String, Int] { def show(s: Int): String = s.toString } + + /** show typeclass for sequences */ + implicit def seq[A](implicit A: Show[String, A]): Show[String, Seq[A]] = + new Show[String, Seq[A]] { + def show(as: Seq[A]): String = as.iterator.map(A.show).mkString("[", ",", "]") + } } diff --git a/tests/src/main/scala/tests.scala b/tests/src/main/scala/tests.scala index 4b0e5c1..4c2a48e 100644 --- a/tests/src/main/scala/tests.scala +++ b/tests/src/main/scala/tests.scala @@ -6,8 +6,6 @@ import contextual.data.scalac._ import contextual.data.fqt._ import contextual.data.txt._ -import scala.util._ - sealed trait Tree[+T] case class Leaf[+L](value: L) extends Tree[L] case class Branch[+B](left: Tree[B], right: Tree[B]) extends Tree[B] @@ -47,6 +45,10 @@ object Test { def apply(a: String, b: String): Test = Test(Param(a, b)) } +case class Account(id: String, emails: String*) + +case class Portfolio(companies: Company*) + object Tests extends TestApp { def tests() = for (i <- 1 to 1000) { @@ -201,5 +203,17 @@ object Tests extends TestApp { test("serialize a value class") { Show.gen[Length].show(new Length(100)) }.assert(_ == "100") + + test("show an Account") { + Show.gen[Account].show(Account("john_doe", "john.doe@yahoo.com", "john.doe@gmail.com")) + }.assert(_ == "Account(id=john_doe,emails=[john.doe@yahoo.com,john.doe@gmail.com])") + + test("construct a default Account") { + Default.gen[Account].default + }.assert(_ == Account("")) + + test("show a Portfolio of Companies") { + Show.gen[Portfolio].show(Portfolio(Company("Alice Inc"), Company("Bob & Co"))) + }.assert(_ == "Portfolio(companies=[Company(name=Alice Inc),Company(name=Bob & Co)])") } } -- cgit v1.2.3