From 579cdb1281e7f3e16b13abc591c5e06b1aa32db5 Mon Sep 17 00:00:00 2001 From: Mathias Date: Thu, 14 Dec 2017 14:20:39 +0100 Subject: Add `CaseClass.rawConstruct` and new `Patcher` example --- core/shared/src/main/scala/interface.scala | 17 +++++++- core/shared/src/main/scala/magnolia.scala | 27 ++++++------ examples/shared/src/main/scala/patch.scala | 69 ++++++++++++++++++++++++++++++ tests/src/main/scala/tests.scala | 41 +++++++++++++++++- 4 files changed, 140 insertions(+), 14 deletions(-) create mode 100644 examples/shared/src/main/scala/patch.scala diff --git a/core/shared/src/main/scala/interface.scala b/core/shared/src/main/scala/interface.scala index 193a6f9..86ef5b1 100644 --- a/core/shared/src/main/scala/interface.scala +++ b/core/shared/src/main/scala/interface.scala @@ -122,7 +122,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 f55dfa7..bc16a62 100644 --- a/core/shared/src/main/scala/magnolia.scala +++ b/core/shared/src/main/scala/magnolia.scala @@ -333,7 +333,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) @@ -381,15 +381,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] @@ -541,9 +544,9 @@ object Magnolia { obj: Boolean, valClass: Boolean, params: Array[Param[Tc, T]], - constructor: (Param[Tc, T] => Any) => 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..ca3cc71 100644 --- a/tests/src/main/scala/tests.scala +++ b/tests/src/main/scala/tests.scala @@ -1,13 +1,14 @@ package magnolia.tests import language.experimental.macros - import magnolia._ import estrapade._ import contextual.data.scalac._ import contextual.data.fqt._ import contextual.data.txt._ +import scala.util.control.NonFatal + 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] @@ -237,6 +238,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) -- cgit v1.2.3 From bcf82e599189fd5bb64e8b248f46caef7734e73e Mon Sep 17 00:00:00 2001 From: Mathias Date: Thu, 14 Dec 2017 14:20:39 +0100 Subject: Add `CaseClass.rawConstruct` and new `Patcher` example --- core/shared/src/main/scala/interface.scala | 17 +++++++- core/shared/src/main/scala/magnolia.scala | 27 ++++++------ examples/shared/src/main/scala/patch.scala | 69 ++++++++++++++++++++++++++++++ tests/src/main/scala/tests.scala | 40 ++++++++++++++++- 4 files changed, 139 insertions(+), 14 deletions(-) create mode 100644 examples/shared/src/main/scala/patch.scala 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) -- cgit v1.2.3