aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMathias <mathias@decodified.com>2017-12-14 14:20:39 +0100
committerMathias <mathias@decodified.com>2017-12-28 14:19:16 +0100
commitbcf82e599189fd5bb64e8b248f46caef7734e73e (patch)
tree3faad616f2a6ab2d1af11fe2f85b5dc3c7e8a77c
parentd4f51be01aba1ff32707a91d44f85272b7c2f562 (diff)
downloadmagnolia-bcf82e599189fd5bb64e8b248f46caef7734e73e.tar.gz
magnolia-bcf82e599189fd5bb64e8b248f46caef7734e73e.tar.bz2
magnolia-bcf82e599189fd5bb64e8b248f46caef7734e73e.zip
Add `CaseClass.rawConstruct` and new `Patcher` example
-rw-r--r--core/shared/src/main/scala/interface.scala17
-rw-r--r--core/shared/src/main/scala/magnolia.scala27
-rw-r--r--examples/shared/src/main/scala/patch.scala69
-rw-r--r--tests/src/main/scala/tests.scala40
4 files changed, 139 insertions, 14 deletions
diff --git a/core/shared/src/main/scala/interface.scala b/core/shared/src/main/scala/interface.scala
index a82a54c..b06f350 100644
--- a/core/shared/src/main/scala/interface.scala
+++ b/core/shared/src/main/scala/interface.scala
@@ -123,7 +123,22 @@ abstract class CaseClass[Typeclass[_], Type] private[magnolia] (
* @param makeParam lambda for converting a generic [[Param]] into the value to be used for
* this parameter in the construction of a new instance of the case class
* @return a new instance of the case class */
- def construct[Return](makeParam: Param[Typeclass, Type] => Return): Type
+ final def construct[Return](makeParam: Param[Typeclass, Type] => Return): Type =
+ rawConstruct(parameters map makeParam)
+
+ /** constructs a new instance of the case class type
+ *
+ * Like [[construct]] this method is implemented by Magnolia and let's 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]]
+ * containing all the field values for the case class type, in order and with the correct types.
+ *
+ * @param fieldValues contains the field values for the case class instance to be constructed,
+ * in order and with the correct types.
+ * @return a new instance of the case class
+ * @throws IllegalArgumentException if the size of `paramValues` differs from the size of [[parameters]] */
+ def rawConstruct(fieldValues: Seq[Any]): Type
/** a sequence of [[Param]] objects representing all of the parameters in the case class
*
diff --git a/core/shared/src/main/scala/magnolia.scala b/core/shared/src/main/scala/magnolia.scala
index 2e7dae8..3445dbc 100644
--- a/core/shared/src/main/scala/magnolia.scala
+++ b/core/shared/src/main/scala/magnolia.scala
@@ -335,7 +335,7 @@ object Magnolia {
val caseParams = caseParamsReversed.reverse
val paramsVal: TermName = TermName(c.freshName("parameters"))
- val fnVal: TermName = TermName(c.freshName("fn"))
+ val fieldValues: TermName = TermName(c.freshName("fieldValues"))
val preAssignments = caseParams.map(_.typeclass)
@@ -383,15 +383,18 @@ object Magnolia {
false,
$isValueClass,
$paramsVal,
- ($fnVal: $magnoliaPkg.Param[$typeConstructor, $genericType] => Any) =>
- new $genericType(..${caseParams.zipWithIndex.map {
- case (typeclass, idx) =>
- val arg = q"$fnVal($paramsVal($idx)).asInstanceOf[${typeclass.paramType}]"
- if (typeclass.repeated) q"$arg: _*" else arg
- }})
- ))
- }"""
- )
+ ($fieldValues: $scalaPkg.Seq[Any]) => {
+ if ($fieldValues.lengthCompare($paramsVal.length) != 0) {
+ val msg = "`" + $className + "` has " + $paramsVal.length + " fields, not " + $fieldValues.size
+ throw new java.lang.IllegalArgumentException(msg)
+ }
+ new $genericType(..${
+ caseParams.zipWithIndex.map { case (typeclass, idx) =>
+ val arg = q"$fieldValues($idx).asInstanceOf[${typeclass.paramType}]"
+ if (typeclass.repeated) q"$arg: _*" else arg
+ }
+ })}))
+ }""")
)
} else if (isSealedTrait) {
val genericSubtypes = classType.get.knownDirectSubclasses.to[List]
@@ -542,9 +545,9 @@ object Magnolia {
obj: Boolean,
valClass: Boolean,
params: Array[Param[Tc, T]],
- constructor: (Param[Tc, T] => Any) => T): CaseClass[Tc, T] =
+ constructor: Seq[Any] => T): CaseClass[Tc, T] =
new CaseClass[Tc, T](name, obj, valClass, params) {
- def construct[R](param: Param[Tc, T] => R): T = constructor(param)
+ def rawConstruct(fieldValues: Seq[Any]): T = constructor(fieldValues)
}
}
diff --git a/examples/shared/src/main/scala/patch.scala b/examples/shared/src/main/scala/patch.scala
new file mode 100644
index 0000000..668a2e2
--- /dev/null
+++ b/examples/shared/src/main/scala/patch.scala
@@ -0,0 +1,69 @@
+package magnolia.examples
+
+import scala.language.experimental.macros
+import magnolia._
+
+/**
+ * Type class for copying an instance of some type `T`,
+ * thereby replacing certain fields with other values.
+ */
+sealed abstract class Patcher[T] {
+
+ /**
+ * Returns a copy of `value` whereby all non-null elements of `fieldValues`
+ * replace the respective fields of `value`.
+ * For all null elements of `fieldValues` the original value of the
+ * respective field of `value` is maintained.
+ *
+ * If the size of `fieldValues` doesn't exactly correspond to the
+ * number of fields of `value` an [[IllegalArgumentException]] is thrown.
+ */
+ def patch(value: T, fieldValues: Seq[Any]): T
+}
+
+object Patcher extends LowerPriorityPatcher {
+
+ type Typeclass[T] = Patcher[T]
+
+ def combine[T](ctx: CaseClass[Patcher, T]): Patcher[T] =
+ new Patcher[T] {
+ def patch(value: T, fieldValues: Seq[Any]): T = {
+ if (fieldValues.lengthCompare(ctx.parameters.size) != 0) {
+ throw new IllegalArgumentException(
+ s"Cannot patch value `$value`, expected ${ctx.parameters.size} fields but got ${fieldValues.size}")
+ }
+ val effectiveFields = ctx.parameters.zip(fieldValues).map {
+ case (param, x) => if (x.asInstanceOf[AnyRef] ne null) x else param dereference value
+ }
+ ctx.rawConstruct(effectiveFields)
+ }
+ }
+
+ def dispatch[T](ctx: SealedTrait[Patcher, T]): Patcher[T] =
+ new Patcher[T] {
+ def patch(value: T, fieldValues: Seq[Any]): T =
+ ctx.dispatch(value)(sub ⇒ sub.typeclass.patch(sub cast value, fieldValues))
+ }
+
+ implicit def gen[T]: Patcher[T] = macro Magnolia.gen[T]
+}
+
+sealed abstract class LowerPriorityPatcher {
+
+ private[this] val _forSingleValue =
+ new Patcher[Any] {
+ def patch(value: Any, fieldValues: Seq[Any]): Any = {
+ if (fieldValues.lengthCompare(1) != 0)
+ throw new IllegalArgumentException(
+ s"Cannot patch single value `$value` with patch sequence of size ${fieldValues.size}")
+ val head = fieldValues.head
+ if (head.getClass != value.getClass)
+ throw new IllegalArgumentException(
+ s"Illegal patch value type. Expected `${value.getClass}` but got `${head.getClass}`")
+ head
+ }
+ }
+
+ // once https://github.com/propensive/magnolia/issues/58 is fixed this can be marked `implicit`
+ def forSingleValue[T]: Patcher[T] = _forSingleValue.asInstanceOf[Patcher[T]]
+} \ No newline at end of file
diff --git a/tests/src/main/scala/tests.scala b/tests/src/main/scala/tests.scala
index e275dee..e947d61 100644
--- a/tests/src/main/scala/tests.scala
+++ b/tests/src/main/scala/tests.scala
@@ -1,7 +1,7 @@
package magnolia.tests
import language.experimental.macros
-
+import scala.util.control.NonFatal
import magnolia._
import estrapade._
import contextual.data.scalac._
@@ -237,6 +237,44 @@ object Tests extends TestApp {
|""")
}
+ test("patch a Person via a Patcher[Entity]") {
+ // these two implicits can be removed once https://github.com/propensive/magnolia/issues/58 is closed
+ implicit val stringPatcher = Patcher.forSingleValue[String]
+ implicit val intPatcher = Patcher.forSingleValue[Int]
+
+ val person = Person("Bob", 42)
+ implicitly[Patcher[Entity]]
+ .patch(person, Seq(null, 21))
+ }.assert(_ == Person("Bob", 21))
+
+ test("throw on an illegal patch attempt with field count mismatch") {
+ // these two implicits can be removed once https://github.com/propensive/magnolia/issues/58 is closed
+ implicit val stringPatcher = Patcher.forSingleValue[String]
+ implicit val intPatcher = Patcher.forSingleValue[Int]
+
+ try {
+ val person = Person("Bob", 42)
+ implicitly[Patcher[Entity]]
+ .patch(person, Seq(null, 21, 'killer))
+ } catch {
+ case NonFatal(e) => e.getMessage
+ }
+ }.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]
+ implicit val intPatcher = Patcher.forSingleValue[Int]
+
+ try {
+ val person = Person("Bob", 42)
+ implicitly[Patcher[Entity]]
+ .patch(person, Seq(null, 'killer))
+ } catch {
+ case NonFatal(e) => e.getMessage
+ }
+ }.assert(_ == "scala.Symbol cannot be cast to java.lang.Integer")
+
class ParentClass() {
case class LocalClass(name: String)