diff --git a/core/src/main/scala/magnolia1/impl.scala b/core/src/main/scala/magnolia1/impl.scala index 7d67c544..9ab2fc31 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))), + CallByNeed.createLazy(tc), + CallByNeed.createValueEvaluator(d), 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] + CallByNeed.createLazy(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..a85c0da7 100644 --- a/core/src/main/scala/magnolia1/interface.scala +++ b/core/src/main/scala/magnolia1/interface.scala @@ -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 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/scalajvm/magnolia1/tests/SerializationTests.scala b/test/src/test/scalajvm/magnolia1/tests/SerializationTests.scala new file mode 100644 index 00000000..63a73c89 --- /dev/null +++ b/test/src/test/scalajvm/magnolia1/tests/SerializationTests.scala @@ -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]]