Skip to content

Commit

Permalink
Merge pull request #499 from RustedBones/tc-serialization-test-scal3
Browse files Browse the repository at this point in the history
[scala3] Add test for serializable generated type-class
  • Loading branch information
adamw authored Nov 12, 2024
2 parents 1bcf4d7 + b231259 commit 035f6e2
Show file tree
Hide file tree
Showing 4 changed files with 87 additions and 11 deletions.
40 changes: 30 additions & 10 deletions core/src/main/scala/magnolia1/impl.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ import scala.reflect.*

import Macro.*

// scala3 lambda generated during derivation reference outer scope
// This fails the typeclass serialization if the outer scope is not serializable
// workaround with this with a serializable fuction
private trait SerializableFunction0[+R] extends Function0[R] with Serializable:
def apply(): R
private trait SerializableFunction1[-T1, +R] extends Function1[T1, R] with Serializable:
def apply(v1: T1): R

object CaseClassDerivation:
inline def fromMirror[Typeclass[_], A](
product: Mirror.ProductOf[A]
Expand Down Expand Up @@ -97,12 +105,17 @@ object CaseClassDerivation:
case _: (EmptyTuple, EmptyTuple) =>
Nil
case _: ((l *: ltail), (p *: ptail)) =>
def unsafeCast(any: Any) = Option.when(any == null || (any: @unchecked).isInstanceOf[p])(any.asInstanceOf[p])
val label = constValue[l].asInstanceOf[String]
val tc = new SerializableFunction0[Typeclass[p]]:
override def apply(): Typeclass[p] = summonInline[Typeclass[p]]

val d = new SerializableFunction0[Option[p]]:
private def unsafeCast(any: Any) = Option.when(any == null || (any: @unchecked).isInstanceOf[p])(any.asInstanceOf[p])
override def apply(): Option[p] = defaults.get(label).flatten.flatMap(d => unsafeCast(d.apply))
paramFromMaps[Typeclass, A, p](
label,
CallByNeed(summonInline[Typeclass[p]]),
CallByNeed.withValueEvaluator(defaults.get(label).flatten.flatMap(d => unsafeCast(d.apply))),
CallByNeed.createLazy(tc),
CallByNeed.createValueEvaluator(d),
repeated,
annotations,
inheritedAnnotations,
Expand Down Expand Up @@ -172,7 +185,16 @@ trait SealedTraitDerivation:
mm.asInstanceOf[m.type],
0
)
case _ =>
case _ => {
val tc = new SerializableFunction0[Typeclass[s]]:
override def apply(): Typeclass[s] = summonFrom {
case tc: Typeclass[`s`] => tc
case _ => deriveSubtype(summonInline[Mirror.Of[s]])
}
val isType = new SerializableFunction1[A, Boolean]:
override def apply(a: A): Boolean = a.isInstanceOf[s & A]
val asType = new SerializableFunction1[A, s & A]:
override def apply(a: A): s & A = a.asInstanceOf[s & A]
List(
new SealedTrait.Subtype[Typeclass, A, s](
typeInfo[s],
Expand All @@ -181,14 +203,12 @@ trait SealedTraitDerivation:
IArray.from(paramTypeAnns[A]),
isObject[s],
idx,
CallByNeed(summonFrom {
case tc: Typeclass[`s`] => tc
case _ => deriveSubtype(summonInline[Mirror.Of[s]])
}),
x => x.isInstanceOf[s & A],
_.asInstanceOf[s & A]
CallByNeed.createLazy(tc),
isType,
asType
)
)
}
}
(sub ::: subtypesFromMirror[A, tail](m, idx + 1)).distinctBy(_.typeInfo).sortBy(_.typeInfo.full)
end SealedTraitDerivation
14 changes: 14 additions & 0 deletions core/src/main/scala/magnolia1/interface.scala
Original file line number Diff line number Diff line change
Expand Up @@ -363,11 +363,25 @@ object CallByNeed:
/** Initializes a class that allows for suspending evaluation of a value until it is needed. Evaluation of a value via `.value` can only
* happen once.
*/
def createLazy[A](a: () => A): CallByNeed[A] = new CallByNeed(a, () => false)

/** Initializes a class that allows for suspending evaluation of a value until it is needed. Evaluation of a value via `.value` can only
* happen once.
*
* If by-name parameter causes serialization issue, use [[createLazy]].
*/
def apply[A](a: => A): CallByNeed[A] = new CallByNeed(() => a, () => false)

/** Initializes a class that allows for suspending evaluation of a value until it is needed. Evaluation of a value via `.value` can only
* happen once. Evaluation of a value via `.valueEvaluator.map(evaluator => evaluator())` will happen every time the evaluator is called
*/
def createValueEvaluator[A](a: () => A): CallByNeed[A] = new CallByNeed(a, () => true)

/** Initializes a class that allows for suspending evaluation of a value until it is needed. Evaluation of a value via `.value` can only
* happen once. Evaluation of a value via `.valueEvaluator.map(evaluator => evaluator())` will happen every time the evaluator is called
*
* If by-name parameter causes serialization issue, use [[withValueEvaluator]].
*/
def withValueEvaluator[A](a: => A): CallByNeed[A] = new CallByNeed(() => a, () => true)
end CallByNeed

Expand Down
2 changes: 1 addition & 1 deletion examples/src/main/scala/magnolia1/examples/show.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import magnolia1._
*
* Note that this is a more general form of `Show` than is usual, as it permits the return type to be something other than a string.
*/
trait Show[Out, T] { def show(value: T): Out }
trait Show[Out, T] extends Serializable { def show(value: T): Out }

trait GenericShow[Out] extends AutoDerivation[[X] =>> Show[Out, X]] {

Expand Down
42 changes: 42 additions & 0 deletions test/src/test/scalajvm/magnolia1/tests/SerializationTests.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package magnolia1.tests

import magnolia1.*
import magnolia1.examples.*

import java.io.*
class SerializationTests extends munit.FunSuite:
import SerializationTests.*

private def serializeToByteArray(value: Serializable): Array[Byte] =
val buffer = new ByteArrayOutputStream()
val oos = new ObjectOutputStream(buffer)
oos.writeObject(value)
buffer.toByteArray

private def deserializeFromByteArray(encodedValue: Array[Byte]): AnyRef =
val ois = new ObjectInputStream(new ByteArrayInputStream(encodedValue))
ois.readObject()

def ensureSerializable[T <: Serializable](value: T): T =
deserializeFromByteArray(serializeToByteArray(value)).asInstanceOf[T]

test("generate serializable type-classes") {
ensureSerializable(new Outer().showAddress)
ensureSerializable(new Outer().showColor)
}

object SerializationTests:
sealed trait Entity
case class Company(name: String) extends Entity
case class Person(name: String, age: Int) extends Entity
case class Address(line1: String, occupant: Person)

sealed trait Color
case object Red extends Color
case object Green extends Color
case object Blue extends Color
case object Orange extends Color
case object Pink extends Color
class Outer:
val showAddress: Show[String, Address] = summon[Show[String, Address]]
val showColor: Show[String, Color] = summon[Show[String, Color]]

0 comments on commit 035f6e2

Please sign in to comment.