Skip to content

Commit

Permalink
Add CaseClass.rawConstruct and new Patcher example
Browse files Browse the repository at this point in the history
  • Loading branch information
sirthias committed Dec 28, 2017
1 parent d4f51be commit bcf82e5
Show file tree
Hide file tree
Showing 4 changed files with 139 additions and 14 deletions.
17 changes: 16 additions & 1 deletion core/shared/src/main/scala/interface.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down
27 changes: 15 additions & 12 deletions core/shared/src/main/scala/magnolia.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)

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

Expand Down
69 changes: 69 additions & 0 deletions examples/shared/src/main/scala/patch.scala
Original file line number Diff line number Diff line change
@@ -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]]
}
40 changes: 39 additions & 1 deletion tests/src/main/scala/tests.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package magnolia.tests

import language.experimental.macros

import scala.util.control.NonFatal
import magnolia._
import estrapade._
import contextual.data.scalac._
Expand Down Expand Up @@ -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)

Expand Down

0 comments on commit bcf82e5

Please sign in to comment.