aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJon Pretty <jon.pretty@propensive.com>2018-02-08 21:37:16 +0000
committerGitHub <noreply@github.com>2018-02-08 21:37:16 +0000
commitb562503348bc36bba09bc5df9a8d846d9bf80912 (patch)
treee867d326898b1e7d0019753728b921c3a160167c
parent4fe64bd5ba7bdc1ea0e29923b27463c62a3c4052 (diff)
parentc1b9c1f53a6bb3f62abc7141e6b6153d8208e1f0 (diff)
downloadmagnolia-b562503348bc36bba09bc5df9a8d846d9bf80912.tar.gz
magnolia-b562503348bc36bba09bc5df9a8d846d9bf80912.tar.bz2
magnolia-b562503348bc36bba09bc5df9a8d846d9bf80912.zip
Merge pull request #74 from kevinwright/feature/attribs-on-params
Feature/attribs on params
-rw-r--r--CONTRIBUTORS1
-rw-r--r--build.sbt11
-rw-r--r--core/shared/src/main/scala/interface.scala42
-rw-r--r--core/shared/src/main/scala/magnolia.scala75
-rw-r--r--examples/shared/src/main/scala/decode.scala26
-rw-r--r--examples/shared/src/main/scala/show.scala38
-rw-r--r--project/build.properties2
-rw-r--r--project/plugins.sbt4
-rw-r--r--tests/src/main/scala/tests.scala30
9 files changed, 154 insertions, 75 deletions
diff --git a/CONTRIBUTORS b/CONTRIBUTORS
index d0d60e5..4e84fbb 100644
--- a/CONTRIBUTORS
+++ b/CONTRIBUTORS
@@ -3,3 +3,4 @@ Loïc Descotte [@loic_d](https://twitter.com/loic_d)
Georgi Krastev [@joro_kr](https://twitter.com/joro_kr/)
Shadaj Laddad [@shadaj](https://twitter.com/shadajl)
Mathias Doenitz [@sirthias](https://twitter.com/sirthias)
+Kevin Wright [@thecoda](https://twitter.com/thecoda)
diff --git a/build.sbt b/build.sbt
index 178ac83..50ec53d 100644
--- a/build.sbt
+++ b/build.sbt
@@ -27,13 +27,20 @@ lazy val tests = project
.settings(moduleName := "magnolia-tests")
.settings(
addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.1" cross CrossVersion.full),
+ initialCommands in console := """import magnolia.tests._; import magnolia.examples._;""",
libraryDependencies ++= Seq(
"com.fommil" %% "deriving-macro" % "0.9.0",
- "com.fommil" %% "scalaz-deriving" % "0.9.0"
+ // These two to allow compilation under Java 9...
+ // Specifically to import XML stuff that got modularised
+ "javax.xml.bind" % "jaxb-api" % "2.3.0" % "compile",
+ "com.sun.xml.bind" % "jaxb-impl" % "2.3.0" % "compile"
)
)
.dependsOn(examplesJVM)
+
+
+
lazy val benchmarks = project
.in(file("benchmarks"))
.settings(buildSettings: _*)
@@ -88,8 +95,6 @@ lazy val publishSettings = Seq(
)
)
-import java.io.File
-
lazy val unmanagedSettings = unmanagedBase := (scalaVersion.value
.split("\\.")
.map(_.toInt)
diff --git a/core/shared/src/main/scala/interface.scala b/core/shared/src/main/scala/interface.scala
index 846eee9..1dac65e 100644
--- a/core/shared/src/main/scala/interface.scala
+++ b/core/shared/src/main/scala/interface.scala
@@ -25,6 +25,8 @@ trait Subtype[Typeclass[_], Type] {
/** partial function defined the subset of values of `Type` which have the type of this subtype */
def cast: PartialFunction[Type, SType]
+
+ override def toString: String = s"Subtype(${typeName.full})"
}
/** represents a parameter of a case class
@@ -89,6 +91,16 @@ trait Param[Typeclass[_], Type] {
* @param param the instance of the case class to be dereferenced
* @return the parameter value */
def dereference(param: Type): PType
+
+ def annotationsArray: Array[Any]
+
+ /** a sequence of objects representing all of the annotations on the case class
+ *
+ * For efficiency, this sequence is implemented by an `Array`, but upcast to a
+ * [[scala.collection.Seq]] to hide the mutable collection API. */
+ final def annotations: Seq[Any] = annotationsArray
+
+ override def toString: String = s"Param($label)"
}
/** represents a case class or case object and the context required to construct a new typeclass
@@ -101,15 +113,18 @@ trait Param[Typeclass[_], Type] {
* @param typeName the name of the case class
* @param isObject true only if this represents a case object rather than a case class
* @param parametersArray an array of [[Param]] values for this case class
+ * @param annotationsArray an array of instantiated annotations applied to this case class
* @tparam Typeclass type constructor for the typeclass being derived
* @tparam Type generic type of this parameter */
abstract class CaseClass[Typeclass[_], Type] private[magnolia] (
val typeName: TypeName,
val isObject: Boolean,
val isValueClass: Boolean,
- parametersArray: Array[Param[Typeclass, Type]]
+ parametersArray: Array[Param[Typeclass, Type]],
+ annotationsArray: Array[Any]
) {
+ override def toString: String = s"CaseClass(${typeName.full}, ${parameters.mkString(",")})"
/** constructs a new instance of the case class type
*
* This method will be implemented by the Magnolia macro to make it possible to construct
@@ -128,7 +143,7 @@ abstract class CaseClass[Typeclass[_], Type] private[magnolia] (
/** constructs a new instance of the case class type
*
- * Like [[construct]] this method is implemented by Magnolia and let's you construct case class
+ * Like [[construct]] this method is implemented by Magnolia and lets you construct case class
* instances generically in user code, without knowing their type concretely.
*
* `rawConstruct`, however, is more low-level in that it expects you to provide a [[Seq]]
@@ -145,6 +160,12 @@ abstract class CaseClass[Typeclass[_], Type] private[magnolia] (
* For efficiency, this sequence is implemented by an `Array`, but upcast to a
* [[scala.collection.Seq]] to hide the mutable collection API. */
final def parameters: Seq[Param[Typeclass, Type]] = parametersArray
+
+ /** a sequence of objects representing all of the annotations on the case class
+ *
+ * For efficiency, this sequence is implemented by an `Array`, but upcast to a
+ * [[scala.collection.Seq]] to hide the mutable collection API. */
+ final def annotations: Seq[Any] = annotationsArray
}
/** represents a sealed trait and the context required to construct a new typeclass instance
@@ -152,13 +173,18 @@ abstract class CaseClass[Typeclass[_], Type] private[magnolia] (
*
* Instances of `SealedTrait` provide access to all of the component subtypes of the sealed trait
* which form a coproduct, and to the fully-qualified name of the sealed trait.
- *
* @param typeName the name of the sealed trait
* @param subtypesArray an array of [[Subtype]] instances for each subtype in the sealed trait
+ * @param annotationsArray an array of instantiated annotations applied to this case class
* @tparam Typeclass type constructor for the typeclass being derived
* @tparam Type generic type of this parameter */
-final class SealedTrait[Typeclass[_], Type](val typeName: TypeName,
- subtypesArray: Array[Subtype[Typeclass, Type]]) {
+final class SealedTrait[Typeclass[_], Type](
+ val typeName: TypeName,
+ subtypesArray: Array[Subtype[Typeclass, Type]],
+ annotationsArray: Array[Any]
+) {
+
+ override def toString: String = s"SealedTrait($typeName, Array[${subtypes.mkString(",")}])"
/** a sequence of all the subtypes of this sealed trait */
def subtypes: Seq[Subtype[Typeclass, Type]] = subtypesArray
@@ -184,6 +210,12 @@ final class SealedTrait[Typeclass[_], Type](val typeName: TypeName,
)
rec(0)
}
+
+ /** a sequence of objects representing all of the annotations on the topmost trait
+ *
+ * For efficiency, this sequence is implemented by an `Array`, but upcast to a
+ * [[scala.collection.Seq]] to hide the mutable collection API. */
+ final def annotations: Seq[Any] = annotationsArray
}
/**
diff --git a/core/shared/src/main/scala/magnolia.scala b/core/shared/src/main/scala/magnolia.scala
index 5318aa2..3cfda3d 100644
--- a/core/shared/src/main/scala/magnolia.scala
+++ b/core/shared/src/main/scala/magnolia.scala
@@ -161,6 +161,8 @@ object Magnolia {
val isCaseObject = classType.exists(_.isModuleClass)
val isSealedTrait = classType.exists(_.isSealed)
+ val classAnnotationTrees = typeSymbol.annotations.map(_.tree)
+
val primitives = Set(typeOf[Double],
typeOf[Float],
typeOf[Short],
@@ -185,11 +187,26 @@ object Magnolia {
val impl = q"""
$typeNameDef
${c.prefix}.combine($magnoliaPkg.Magnolia.caseClass[$typeConstructor, $genericType](
- $typeName, true, false, new $scalaPkg.Array(0), _ => ${genericType.typeSymbol.asClass.module})
- )
+ $typeName,
+ true,
+ false,
+ new $scalaPkg.Array(0),
+ $scalaPkg.Array(..$classAnnotationTrees),
+ _ => ${genericType.typeSymbol.asClass.module}
+ ))
"""
Some(impl)
} else if (isCaseClass || isValueClass) {
+
+ val companionRef = GlobalUtil.patchedCompanionRef(c)(genericType.dealias)
+
+ val headParamList = {
+ val primaryConstructor = classType map (_.primaryConstructor)
+ val optList: Option[List[c.universe.Symbol]] =
+ primaryConstructor flatMap (_.asMethod.typeSignature.paramLists.headOption)
+ optList.map(_.map(_.asTerm))
+ }
+
val caseClassParameters = genericType.decls.collect {
case m: MethodSymbol if m.isCaseAccessor || (isValueClass && m.isParamAccessor) =>
m.asMethod
@@ -199,7 +216,8 @@ object Magnolia {
repeated: Boolean,
typeclass: Tree,
paramType: Type,
- ref: TermName)
+ ref: TermName
+ )
val caseParamsReversed = caseClassParameters.foldLeft[List[CaseParam]](Nil) {
(acc, param) =>
@@ -238,20 +256,17 @@ object Magnolia {
val preAssignments = caseParams.map(_.typeclass)
- val defaults = if (!isValueClass) {
- val companionRef = GlobalUtil.patchedCompanionRef(c)(genericType.dealias)
- val companionSym = companionRef.symbol.asModule.info
- // If a companion object is defined with alternative apply methods
- // it is needed get all the alternatives
- val constructorMethods =
- companionSym.decl(TermName("apply")).alternatives.map(_.asMethod)
+ val defaults = headParamList map { plist =>
+ // note: This causes the namer/typer to generate the synthetic default methods by forcing
+ // the typeSignature of the "default" factory method to be visited.
+ // It feels like it shouldn't be needed, but we'll get errors otherwise (as discovered after 6 hours debugging)
- // The last apply method in the alternatives is the one that belongs
- // to the case class, not the user defined companion object
- val indexedConstructorParams =
- constructorMethods.last.paramLists.head.map(_.asTerm).zipWithIndex
+ val companionSym = companionRef.symbol.asModule.info
+ val primaryFactoryMethod = companionSym.decl(TermName("apply")).alternatives.lastOption
+ primaryFactoryMethod.foreach(_.asMethod.typeSignature)
+ val indexedConstructorParams = plist.zipWithIndex
indexedConstructorParams.map {
case (p, idx) =>
if (p.isParamWithDefault) {
@@ -259,13 +274,22 @@ object Magnolia {
q"$scalaPkg.Some($companionRef.$method)"
} else q"$scalaPkg.None"
}
- } else List(q"$scalaPkg.None")
+ } getOrElse List(q"$scalaPkg.None")
+
+ val annotations: List[List[Tree]] = headParamList.toList.flatten map { param =>
+ param.annotations map { _.tree }
+ }
- val assignments = caseParams.zip(defaults).zipWithIndex.map {
- case ((CaseParam(param, repeated, typeclass, paramType, ref), defaultVal), idx) =>
+ val assignments = caseParams.zip(defaults).zip(annotations).zipWithIndex.map {
+ case (((CaseParam(param, repeated, typeclass, paramType, ref), defaultVal), annList), idx) =>
q"""$paramsVal($idx) = $magnoliaPkg.Magnolia.param[$typeConstructor, $genericType,
$paramType](
- ${param.name.decodedName.toString}, $repeated, $ref, $defaultVal, _.${param.name}
+ ${param.name.decodedName.toString},
+ $repeated,
+ $ref,
+ $defaultVal,
+ _.${param.name},
+ $scalaPkg.Array(..$annList)
)"""
}
@@ -282,6 +306,7 @@ object Magnolia {
false,
$isValueClass,
$paramsVal,
+ $scalaPkg.Array(..$classAnnotationTrees),
($fieldValues: $scalaPkg.Seq[Any]) => {
if ($fieldValues.lengthCompare($paramsVal.length) != 0) {
val msg = "`" + $typeName.full + "` has " + $paramsVal.length + " fields, not " + $fieldValues.size
@@ -343,8 +368,9 @@ object Magnolia {
${c.prefix}.dispatch(new $magnoliaPkg.SealedTrait(
$typeName,
- $subtypesVal: $scalaPkg.Array[$magnoliaPkg.Subtype[$typeConstructor, $genericType]])
- ): $resultType
+ $subtypesVal: $scalaPkg.Array[$magnoliaPkg.Subtype[$typeConstructor, $genericType]],
+ $scalaPkg.Array(..$classAnnotationTrees)
+ )): $resultType
}""")
} else None
@@ -398,6 +424,7 @@ object Magnolia {
def cast: PartialFunction[T, SType] = this
def isDefinedAt(t: T) = isType(t)
def apply(t: T): SType = asType(t)
+ override def toString: String = s"Subtype(${typeName.full})"
}
/** constructs a new [[Param]] instance
@@ -408,13 +435,16 @@ object Magnolia {
isRepeated: Boolean,
typeclassParam: => Tc[P],
defaultVal: => Option[P],
- deref: T => P): Param[Tc, T] = new Param[Tc, T] {
+ deref: T => P,
+ annotationsArrayParam: Array[Any]
+ ): Param[Tc, T] = 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)
+ def annotationsArray: Array[Any] = annotationsArrayParam
}
/** constructs a new [[CaseClass]] instance
@@ -425,8 +455,9 @@ object Magnolia {
obj: Boolean,
valClass: Boolean,
params: Array[Param[Tc, T]],
+ annotations: Array[Any],
constructor: Seq[Any] => T): CaseClass[Tc, T] =
- new CaseClass[Tc, T](name, obj, valClass, params) {
+ new CaseClass[Tc, T](name, obj, valClass, params, annotations) {
def rawConstruct(fieldValues: Seq[Any]): T = constructor(fieldValues)
}
}
diff --git a/examples/shared/src/main/scala/decode.scala b/examples/shared/src/main/scala/decode.scala
index 595ee6f..539e478 100644
--- a/examples/shared/src/main/scala/decode.scala
+++ b/examples/shared/src/main/scala/decode.scala
@@ -10,12 +10,10 @@ trait Decoder[T] { def decode(str: String): T }
object Decoder {
/** decodes strings */
- implicit val string: Decoder[String] = new Decoder[String] {
- def decode(str: String): String = str
- }
+ implicit val string: Decoder[String] = (s: String) => s
/** decodes ints */
- implicit val int: Decoder[Int] = new Decoder[Int] { def decode(str: String): Int = str.toInt }
+ implicit val int: Decoder[Int] = _.toInt
/** binds the Magnolia macro to this derivation object */
implicit def gen[T]: Decoder[T] = macro Magnolia.gen[T]
@@ -24,22 +22,18 @@ object Decoder {
type Typeclass[T] = Decoder[T]
/** defines how new [[Decoder]]s for case classes should be constructed */
- def combine[T](ctx: CaseClass[Decoder, T]): Decoder[T] = new Decoder[T] {
- def decode(value: String) = {
- val (_, values) = parse(value)
- ctx.construct { param =>
- param.typeclass.decode(values(param.label))
- }
+ def combine[T](ctx: CaseClass[Decoder, T]): Decoder[T] = value => {
+ val (_, values) = parse(value)
+ ctx.construct { param =>
+ param.typeclass.decode(values(param.label))
}
}
/** defines how to choose which subtype of the sealed trait to use for decoding */
- def dispatch[T](ctx: SealedTrait[Decoder, T]): Decoder[T] = new Decoder[T] {
- def decode(param: String) = {
- val (name, _) = parse(param)
- val subtype = ctx.subtypes.find(_.typeName.full == name).get
- subtype.typeclass.decode(param)
- }
+ def dispatch[T](ctx: SealedTrait[Decoder, T]): Decoder[T] = param => {
+ val (name, _) = parse(param)
+ val subtype = ctx.subtypes.find(_.typeName.full == name).get
+ subtype.typeclass.decode(param)
}
/** very simple extractor for grabbing an entire parameter value, assuming matching parentheses */
diff --git a/examples/shared/src/main/scala/show.scala b/examples/shared/src/main/scala/show.scala
index 6f5838b..ecf1dec 100644
--- a/examples/shared/src/main/scala/show.scala
+++ b/examples/shared/src/main/scala/show.scala
@@ -20,26 +20,30 @@ trait GenericShow[Out] {
/** creates a new [[Show]] instance by labelling and joining (with `mkString`) the result of
* showing each parameter, and prefixing it with the class name */
- def combine[T](ctx: CaseClass[Typeclass, T]): Show[Out, T] = new Show[Out, T] {
- def show(value: T) =
- if (ctx.isValueClass) {
- val param = ctx.parameters.head
- param.typeclass.show(param.dereference(value))
- } else {
- val paramStrings = ctx.parameters.map { param =>
- s"${param.label}=${param.typeclass.show(param.dereference(value))}"
+ def combine[T](ctx: CaseClass[Typeclass, T]): Show[Out, T] = { value =>
+ if (ctx.isValueClass) {
+ val param = ctx.parameters.head
+ param.typeclass.show(param.dereference(value))
+ } else {
+ val paramStrings = ctx.parameters.map { param =>
+ val attribStr = if(param.annotations.isEmpty) "" else {
+ param.annotations.mkString("{", ", ", "}")
}
-
- join(ctx.typeName.short, paramStrings)
+ s"${param.label}$attribStr=${param.typeclass.show(param.dereference(value))}"
}
+
+ val anns = ctx.annotations.filterNot(_.isInstanceOf[scala.SerialVersionUID])
+ val annotationStr = if (anns.isEmpty) "" else anns.mkString("{", ",", "}")
+
+ join(ctx.typeName.short + annotationStr, paramStrings)
+ }
}
/** choose which typeclass to use based on the subtype of the sealed trait */
- def dispatch[T](ctx: SealedTrait[Typeclass, T]): Show[Out, T] = new Show[Out, T] {
- def show(value: T): Out = ctx.dispatch(value) { sub =>
+ def dispatch[T](ctx: SealedTrait[Typeclass, T]): Show[Out, T] = (value: T) =>
+ ctx.dispatch(value) { sub =>
sub.typeclass.show(sub.cast(value))
}
- }
/** bind the Magnolia macro to this derivation object */
implicit def gen[T]: Show[Out, T] = macro Magnolia.gen[T]
@@ -49,17 +53,13 @@ trait GenericShow[Out] {
object Show extends GenericShow[String] {
/** show typeclass for strings */
- implicit val string: Show[String, String] = new Show[String, String] {
- def show(s: String): String = s
- }
+ implicit val string: Show[String, String] = (s: String) => s
def join(typeName: String, params: Seq[String]): String =
params.mkString(s"$typeName(", ",", ")")
/** show typeclass for integers */
- implicit val int: Show[String, Int] = new Show[String, Int] {
- def show(s: Int): String = s.toString
- }
+ implicit val int: Show[String, Int] = (s: Int) => s.toString
/** show typeclass for sequences */
implicit def seq[A](implicit A: Show[String, A]): Show[String, Seq[A]] =
diff --git a/project/build.properties b/project/build.properties
index 9abea12..8b697bb 100644
--- a/project/build.properties
+++ b/project/build.properties
@@ -1 +1 @@
-sbt.version=1.0.3
+sbt.version=1.1.0
diff --git a/project/plugins.sbt b/project/plugins.sbt
index 4a92b89..595dbcc 100644
--- a/project/plugins.sbt
+++ b/project/plugins.sbt
@@ -1,5 +1,5 @@
-addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.0-M1")
-addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "2.0")
+addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.0")
+addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "2.1")
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.5.1")
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.21")
diff --git a/tests/src/main/scala/tests.scala b/tests/src/main/scala/tests.scala
index a311291..9153325 100644
--- a/tests/src/main/scala/tests.scala
+++ b/tests/src/main/scala/tests.scala
@@ -1,12 +1,13 @@
package magnolia.tests
import language.experimental.macros
-import estrapade.{test, TestApp}
+import estrapade.{TestApp, test}
import contextual.data.scalac._
import contextual.data.fqt._
import contextual.data.txt._
import magnolia.examples._
+import scala.annotation.StaticAnnotation
import scala.util.control.NonFatal
sealed trait Tree[+T]
@@ -29,8 +30,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 }
+ implicit val showFruit: Show[String, Fruit] = (f: Fruit) => f.name
}
case class Fruit(name: String)
@@ -41,6 +41,14 @@ case object Red extends Color
case object Green extends Color
case object Blue extends Color
+
+case class MyAnnotation(order: Int) extends StaticAnnotation
+
+@MyAnnotation(0) case class Attributed(
+ @MyAnnotation(1) p1: String,
+ @MyAnnotation(2) p2: Int
+)
+
case class `%%`(`/`: Int, `#`: String)
case class Param(a: String, b: String)
@@ -245,6 +253,7 @@ object Tests extends TestApp {
}
}.assert(_ == "Cannot patch value `Person(Bob,42)`, expected 2 fields but got 3")
+
test("throw on an illegal patch attempt with field type mismatch") {
// these two implicits can be removed once https://github.com/propensive/magnolia/issues/58 is closed
implicit val stringPatcher = Patcher.forSingleValue[String]
@@ -253,10 +262,14 @@ object Tests extends TestApp {
try {
val person = Person("Bob", 42)
implicitly[Patcher[Entity]].patch(person, Seq(null, 'killer))
+ "it worked"
} catch {
case NonFatal(e) => e.getMessage
}
- }.assert(_ == "scala.Symbol cannot be cast to java.lang.Integer")
+ }.assert{x =>
+ //tiny hack because Java 9 inserts the "java.base/" module name in the error message
+ x.startsWith("scala.Symbol cannot be cast to") && x.endsWith("java.lang.Integer")
+ }
class ParentClass {
case class InnerClass(name: String)
@@ -349,11 +362,14 @@ object Tests extends TestApp {
implicit def showDefaultOption[A](
implicit showA: Show[String, A],
defaultA: Default[A]
- ): Show[String, Option[A]] = new Show[String, Option[A]] {
- def show(optA: Option[A]) = showA.show(optA.getOrElse(defaultA.default))
- }
+ ): Show[String, Option[A]] = (optA: Option[A]) => showA.show(optA.getOrElse(defaultA.default))
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("capture attributes against params") {
+ Show.gen[Attributed].show(Attributed("xyz", 100))
+ }.assert(_ == "Attributed{MyAnnotation(0)}(p1{MyAnnotation(1)}=xyz,p2{MyAnnotation(2)}=100)")
+
}
}