From 808609dd5cfb12317ce7fd8011053fa292b319b4 Mon Sep 17 00:00:00 2001 From: Michel Davit Date: Mon, 13 Nov 2023 13:15:30 +0100 Subject: [PATCH 1/4] Typeclass serialization tests --- .../main/scala/magnolia1/examples/show.scala | 2 +- .../magnolia1/tests/SerializationTests.scala | 43 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 test/src/test/scala/magnolia1/tests/SerializationTests.scala diff --git a/examples/src/main/scala/magnolia1/examples/show.scala b/examples/src/main/scala/magnolia1/examples/show.scala index 1e3f543c..f46a7067 100644 --- a/examples/src/main/scala/magnolia1/examples/show.scala +++ b/examples/src/main/scala/magnolia1/examples/show.scala @@ -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]] { diff --git a/test/src/test/scala/magnolia1/tests/SerializationTests.scala b/test/src/test/scala/magnolia1/tests/SerializationTests.scala new file mode 100644 index 00000000..427b05b1 --- /dev/null +++ b/test/src/test/scala/magnolia1/tests/SerializationTests.scala @@ -0,0 +1,43 @@ +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]] + From 89a23e271232197077f29d664786a7b214bfb922 Mon Sep 17 00:00:00 2001 From: Michel Davit Date: Sun, 7 Jan 2024 11:11:59 +0100 Subject: [PATCH 2/4] Make typeclass serializable format --- core/src/main/scala/magnolia1/impl.scala | 40 ++++++++++++++----- core/src/main/scala/magnolia1/interface.scala | 3 +- .../magnolia1/tests/SerializationTests.scala | 1 - 3 files changed, 31 insertions(+), 13 deletions(-) diff --git a/core/src/main/scala/magnolia1/impl.scala b/core/src/main/scala/magnolia1/impl.scala index 7d67c544..1d51953a 100644 --- a/core/src/main/scala/magnolia1/impl.scala +++ b/core/src/main/scala/magnolia1/impl.scala @@ -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] @@ -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))), + new CallByNeed(tc), + new CallByNeed(d, () => true), repeated, annotations, inheritedAnnotations, @@ -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], @@ -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] + new CallByNeed(tc), + isType, + asType ) ) + } } (sub ::: subtypesFromMirror[A, tail](m, idx + 1)).distinctBy(_.typeInfo).sortBy(_.typeInfo.full) end SealedTraitDerivation diff --git a/core/src/main/scala/magnolia1/interface.scala b/core/src/main/scala/magnolia1/interface.scala index 42f14002..57453c8e 100644 --- a/core/src/main/scala/magnolia1/interface.scala +++ b/core/src/main/scala/magnolia1/interface.scala @@ -373,8 +373,7 @@ end CallByNeed // Both params are later nullified to reduce overhead and increase performance. // The supportDynamicValueEvaluation is passed as a function so that it can be nullified. Otherwise, there is no need for the function value. -final class CallByNeed[+A] private (private[this] var eval: () => A, private var supportDynamicValueEvaluation: () => Boolean) - extends Serializable { +final class CallByNeed[+A](private[this] var eval: () => A, private var supportDynamicValueEvaluation: () => Boolean) extends Serializable { // This second constructor is necessary to support backwards compatibility for v1.3.6 and earlier def this(eval: () => A) = this(eval, () => false) diff --git a/test/src/test/scala/magnolia1/tests/SerializationTests.scala b/test/src/test/scala/magnolia1/tests/SerializationTests.scala index 427b05b1..63a73c89 100644 --- a/test/src/test/scala/magnolia1/tests/SerializationTests.scala +++ b/test/src/test/scala/magnolia1/tests/SerializationTests.scala @@ -40,4 +40,3 @@ object SerializationTests: class Outer: val showAddress: Show[String, Address] = summon[Show[String, Address]] val showColor: Show[String, Color] = summon[Show[String, Color]] - From 3a1f7473e8abca394fadd8ce5b027f11b728bac7 Mon Sep 17 00:00:00 2001 From: Michel Davit Date: Tue, 12 Nov 2024 15:08:33 +0100 Subject: [PATCH 3/4] Move serialization tests to jvm folder --- .../{scala => scalajvm}/magnolia1/tests/SerializationTests.scala | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/src/test/{scala => scalajvm}/magnolia1/tests/SerializationTests.scala (100%) diff --git a/test/src/test/scala/magnolia1/tests/SerializationTests.scala b/test/src/test/scalajvm/magnolia1/tests/SerializationTests.scala similarity index 100% rename from test/src/test/scala/magnolia1/tests/SerializationTests.scala rename to test/src/test/scalajvm/magnolia1/tests/SerializationTests.scala From b231259edd357e036effbb37ef640dfd64882660 Mon Sep 17 00:00:00 2001 From: Michel Davit Date: Tue, 12 Nov 2024 17:36:13 +0100 Subject: [PATCH 4/4] by-value API --- core/src/main/scala/magnolia1/impl.scala | 6 +++--- core/src/main/scala/magnolia1/interface.scala | 17 ++++++++++++++++- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/core/src/main/scala/magnolia1/impl.scala b/core/src/main/scala/magnolia1/impl.scala index 1d51953a..9ab2fc31 100644 --- a/core/src/main/scala/magnolia1/impl.scala +++ b/core/src/main/scala/magnolia1/impl.scala @@ -114,8 +114,8 @@ object CaseClassDerivation: override def apply(): Option[p] = defaults.get(label).flatten.flatMap(d => unsafeCast(d.apply)) paramFromMaps[Typeclass, A, p]( label, - new CallByNeed(tc), - new CallByNeed(d, () => true), + CallByNeed.createLazy(tc), + CallByNeed.createValueEvaluator(d), repeated, annotations, inheritedAnnotations, @@ -203,7 +203,7 @@ trait SealedTraitDerivation: IArray.from(paramTypeAnns[A]), isObject[s], idx, - new CallByNeed(tc), + CallByNeed.createLazy(tc), isType, asType ) diff --git a/core/src/main/scala/magnolia1/interface.scala b/core/src/main/scala/magnolia1/interface.scala index 57453c8e..a85c0da7 100644 --- a/core/src/main/scala/magnolia1/interface.scala +++ b/core/src/main/scala/magnolia1/interface.scala @@ -363,17 +363,32 @@ 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 // Both params are later nullified to reduce overhead and increase performance. // The supportDynamicValueEvaluation is passed as a function so that it can be nullified. Otherwise, there is no need for the function value. -final class CallByNeed[+A](private[this] var eval: () => A, private var supportDynamicValueEvaluation: () => Boolean) extends Serializable { +final class CallByNeed[+A] private (private[this] var eval: () => A, private var supportDynamicValueEvaluation: () => Boolean) + extends Serializable { // This second constructor is necessary to support backwards compatibility for v1.3.6 and earlier def this(eval: () => A) = this(eval, () => false)