diff --git a/core/src/main/scala/magnolia1/parameterised/impl.scala b/core/src/main/scala/magnolia1/parameterised/impl.scala new file mode 100644 index 00000000..c9422988 --- /dev/null +++ b/core/src/main/scala/magnolia1/parameterised/impl.scala @@ -0,0 +1,71 @@ +package magnolia1.parameterised + +import scala.compiletime.* +import scala.deriving.Mirror +import scala.reflect.* + +import magnolia1.* +import magnolia1.Macro.* + +trait ParamaterisedSealedTraitDerivation: + type Typeclass[T] + type Ps[T] + + protected inline def deriveSubtype[s]( + m: Mirror.Of[s], + i: Ps[s] + ): Typeclass[s] + + protected inline def sealedTraitFromMirror[A]( + m: Mirror.SumOf[A] + ): SealedTrait[Typeclass, A] = + SealedTrait( + typeInfo[A], + IArray(subtypesFromMirror[A, m.MirroredElemTypes](m)*), + IArray.from(anns[A]), + IArray(paramTypeAnns[A]*), + isEnum[A], + IArray.from(inheritedAnns[A]) + ) + + protected transparent inline def subtypesFromMirror[A, SubtypeTuple <: Tuple]( + m: Mirror.SumOf[A], + result: List[SealedTrait.Subtype[Typeclass, A, _]] = Nil + ): List[SealedTrait.Subtype[Typeclass, A, _]] = + inline erasedValue[SubtypeTuple] match + case _: EmptyTuple => + result.distinctBy(_.typeInfo).sortBy(_.typeInfo.full) + case _: (s *: tail) => + val sub = summonFrom { + case mm: Mirror.SumOf[`s`] => + subtypesFromMirror[A, mm.MirroredElemTypes]( + mm.asInstanceOf[m.type], + Nil + ) + case _ => { + val tc = new SerializableFunction0[Typeclass[s]]: + override def apply(): Typeclass[s] = summonFrom { + case tc: Typeclass[`s`] => tc + case _ => deriveSubtype(summonInline[Mirror.Of[s]], summonInline[Ps[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], + IArray.from(anns[s]), + IArray.from(inheritedAnns[s]), + IArray.from(paramTypeAnns[A]), + isObject[s], + 0, // unused + CallByNeed.createLazy(tc), + isType, + asType + ) + ) + } + } + subtypesFromMirror[A, tail](m, sub ::: result) +end ParamaterisedSealedTraitDerivation diff --git a/core/src/main/scala/magnolia1/parameterised/paramaterised_derivation.scala b/core/src/main/scala/magnolia1/parameterised/paramaterised_derivation.scala new file mode 100644 index 00000000..50f64354 --- /dev/null +++ b/core/src/main/scala/magnolia1/parameterised/paramaterised_derivation.scala @@ -0,0 +1,137 @@ +package magnolia1.parameterised + +import magnolia1.* + +import scala.compiletime.{erasedValue, summonFrom, summonInline} +import scala.deriving.Mirror +import scala.quoted.Expr + +trait ParamaterisedCommonDerivation[TypeClass[_], PS[_]]: + type Typeclass[T] = TypeClass[T] + type Ps[T] = PS[T] + + /** Must be implemented by the user of Magnolia to construct a typeclass for case class `T` using the provided type info. E.g. if we are + * deriving `Show[T]` typeclasses, and `T` is a case class `Foo(...)`, we need to constuct `Show[Foo]`. + * + * This method is called 'join' because typically it will _join_ together the typeclasses for all the parameters of the case class, into + * a single typeclass for the case class itself. The field [[CaseClass.params]] can provide useful information for doing this. + * + * @param caseClass + * information about the case class `T`, its parameters, and _their_ typeclasses + */ + def join[T: Ps](caseClass: CaseClass[Typeclass, T]): Typeclass[T] + + inline def derivedMirrorProduct[A: Ps]( + product: Mirror.ProductOf[A] + ): Typeclass[A] = join(CaseClassDerivation.fromMirror(product)) + + inline def getParams__[T, Labels <: Tuple, Params <: Tuple]( + annotations: Map[String, List[Any]], + inheritedAnnotations: Map[String, List[Any]], + typeAnnotations: Map[String, List[Any]], + repeated: Map[String, Boolean], + defaults: Map[String, Option[() => Any]], + idx: Int = 0 + ): List[CaseClass.Param[Typeclass, T]] = CaseClassDerivation.paramsFromMaps( + annotations, + inheritedAnnotations, + typeAnnotations, + repeated, + defaults + ) + + // for backward compatibility with v1.1.1 + inline def getParams_[T, Labels <: Tuple, Params <: Tuple]( + annotations: Map[String, List[Any]], + inheritedAnnotations: Map[String, List[Any]], + typeAnnotations: Map[String, List[Any]], + repeated: Map[String, Boolean], + idx: Int = 0 + ): List[CaseClass.Param[Typeclass, T]] = + getParams__(annotations, Map.empty, typeAnnotations, repeated, Map(), idx) + + // for backward compatibility with v1.0.0 + inline def getParams[T, Labels <: Tuple, Params <: Tuple]( + annotations: Map[String, List[Any]], + typeAnnotations: Map[String, List[Any]], + repeated: Map[String, Boolean], + idx: Int = 0 + ): List[CaseClass.Param[Typeclass, T]] = + getParams__(annotations, Map.empty, typeAnnotations, repeated, Map(), idx) + +end ParamaterisedCommonDerivation + +trait ParamaterisedProductDerivation[TypeClass[_], PS[_]] extends ParamaterisedCommonDerivation[TypeClass, PS]: + inline def derivedMirror[A](using mirror: Mirror.Of[A], ps: PS[A]): Typeclass[A] = + inline mirror match + case product: Mirror.ProductOf[A] => derivedMirrorProduct[A](product) + + inline given derived[A](using mirror: Mirror.Of[A]): Typeclass[A] = + summonFrom { + case p: PS[A] => derivedMirror[A](using mirror, p) + case _ => derivedMirror[A](using mirror, default[A]) + } + + def default[A]: PS[A] +end ParamaterisedProductDerivation + +trait ParamaterisedDerivation[TypeClass[_], PS[_]] + extends ParamaterisedCommonDerivation[TypeClass, PS] + with ParamaterisedSealedTraitDerivation: + + /** This must be implemented by the user of Magnolia to construct a Typeclass for 'T', where 'T' is a Sealed Trait or Scala 3 Enum, using + * the provided type info. E.g. if we are deriving 'Show[T]' typeclasses, and T is an enum 'Suit' (eg with values Diamonds, Clubs, etc), + * we need to constuct 'Show[Suit]'. + * + * This method is called 'split' because it will ''split'' the different possible types of the SealedTrait, and handle each one to + * finally produce a typeclass capable of handling any possible subtype of the trait. + * + * A useful function for implementing this method is [[SealedTrait#choose]], which can take a value instance and provide information on + * the specific subtype of the sealedTrait which that value is. + */ + def split[T: PS](sealedTrait: SealedTrait[Typeclass, T]): Typeclass[T] + + /** This must be implemented by the user in order for value class support to work + */ + def handleAnyVal: [T <: AnyVal, S] => (WrapAndSerde[T, Typeclass, S], UnwrapAndSerde[T, Typeclass, S]) => Typeclass[S] => Typeclass[T] + + transparent inline def subtypes[T, SubtypeTuple <: Tuple]( + m: Mirror.SumOf[T] + ): List[SealedTrait.Subtype[Typeclass, T, _]] = + subtypesFromMirror[T, SubtypeTuple](m) + + inline def derivedMirrorSum[A: PS](sum: Mirror.SumOf[A]): Typeclass[A] = + split(sealedTraitFromMirror(sum)) + + inline def derivedMirror[A](using mirror: Mirror.Of[A], i: PS[A]): Typeclass[A] = + inline mirror match + case sum: Mirror.SumOf[A] => derivedMirrorSum[A](sum) + case product: Mirror.ProductOf[A] => derivedMirrorProduct[A](product) + + inline def derived[A](using mirror: Mirror.Of[A]): Typeclass[A] = + summonFrom { + case p: PS[A] => derivedMirror[A](using mirror, p) + case _ => derivedMirror[A](using mirror, default[A]) + } + + protected override inline def deriveSubtype[s]( + m: Mirror.Of[s], + i: PS[s] + ): Typeclass[s] = derivedMirror[s](using m, i) + inline def derived[T <: AnyVal & Product: PS]: TypeClass[T] = deriveAnyValSupport[T] + + inline def handlePrimitive[A]: TypeClass[A] = summonInline[Typeclass[A]] + inline implicit def deriveAnyValSupport[A <: AnyVal & Product: PS]: TypeClass[A] = + ${ + ValueClassSupport.deriveAnyValSupportImpl[A, Typeclass, this.type]('handleAnyVal, 'this) + } + + // alias because I can't write macros well enough to avoid it r/n + inline def mirrorDerived[A](using mirror: Mirror.Of[A]): Typeclass[A] = derived[A] + inline def noMirrorDerived[A]: Typeclass[A] = handlePrimitive[A] + def default[A]: PS[A] = throw new IllegalStateException("No default implemented") +end ParamaterisedDerivation + +trait ParamaterisedAutoDerivation[TypeClass[_], PS[_]] extends ParamaterisedDerivation[TypeClass, PS]: + inline given autoDerived[A](using Mirror.Of[A], PS[A]): TypeClass[A] = derivedMirror[A] + inline given autoDerived[A <: AnyVal & Product: PS]: TypeClass[A] = derived[A] diff --git a/core/src/main/scala/magnolia1/parameterised/valueclass_support.scala b/core/src/main/scala/magnolia1/parameterised/valueclass_support.scala new file mode 100644 index 00000000..5426ec16 --- /dev/null +++ b/core/src/main/scala/magnolia1/parameterised/valueclass_support.scala @@ -0,0 +1,97 @@ +package magnolia1.parameterised + +object ValueClassSupport { + + import scala.quoted.* + + def deriveAnyValSupportImpl[A <: AnyVal, Typeclass[_]: Type, DerivedClass <: ParamaterisedDerivation[Typeclass, ?]]( + fn: Expr[[T <: AnyVal, S] => (WrapAndSerde[T, Typeclass, S], UnwrapAndSerde[T, Typeclass, S]) => Typeclass[S] => Typeclass[T]], + self: Expr[DerivedClass] + )(using quotes: Quotes, tpe: Type[A]): Expr[Typeclass[A]] = { + import quotes.*, quotes.reflect.* + val wrapperSym = TypeRepr.of[A].typeSymbol + val constructor = wrapperSym.primaryConstructor + val arg = constructor.paramSymss.head.head + val argName = arg.name + val theType = arg.tree match { + case ValDef(_, tt: TypeTree, _) => tt + case _ => + quotes.reflect.report.errorAndAbort( + "expecting AnyVal with Product to have a single constructor arg" + ) + } + theType.tpe.asType match { + case '[t] => + val encoder = + if (theType.symbol.declaredFields.nonEmpty) + Implicits.search(TypeRepr.of[scala.deriving.Mirror.Of[t]]) match { + case mirror: ImplicitSearchSuccess => + Apply( + TypeApply( + Select.unique(self.asTerm, "mirrorDerived"), + List(theType) + ), + List(mirror.tree) + ) + case _ => + report.errorAndAbort(s"Unable to find constructor for type ${theType.show}") + } + else + TypeApply( + Select.unique(self.asTerm, "noMirrorDerived"), + List(theType) + ) + val applyMtpe = MethodType(List("v"))(_ => List(TypeRepr.of[t]), _ => TypeRepr.of[A]) + + def doApply = Lambda( + Symbol.noSymbol, + applyMtpe, + { + case (_, List(arg)) => + Apply( + Select(New(TypeIdent(wrapperSym)), constructor), + List(Ref(arg.symbol)) + ) + case _ => + quotes.reflect.report.errorAndAbort( + "expecting AnyVal constructor to be called with a single arg" + ) + } + ).asExprOf[t => A] + + val unapplyMtpe = MethodType(List("v"))(_ => List(TypeRepr.of[A]), _ => TypeRepr.of[t]) + def doUnapply = Lambda( + Symbol.noSymbol, + unapplyMtpe, + { + case (_, List(arg)) => + Select(Ref(arg.symbol), wrapperSym.fieldMember(argName)) + case _ => + quotes.reflect.report.errorAndAbort( + "expecting AnyVal constructor to be called with a single arg" + ) + } + ).asExprOf[A => t] + + '{ + FullDerivedClassSupp[A, Typeclass, t]( + WrapAndSerde[A, Typeclass, t](${ doApply })(using ${ encoder.asExprOf[Typeclass[t]] }), + UnwrapAndSerde[A, Typeclass, t](${ doUnapply })(using ${ encoder.asExprOf[Typeclass[t]] }), + ${ fn }[A, t] + ).tc(using ${ encoder.asExprOf[Typeclass[t]] }) + }.asExprOf[Typeclass[A]] + } + } +} + +case class FullDerivedClassSupp[S <: AnyVal, Typeclass[_], T]( + wrapAndSerde: WrapAndSerde[S, Typeclass, T], + unwrapAndSerde: UnwrapAndSerde[S, Typeclass, T], + fn: (WrapAndSerde[S, Typeclass, T], UnwrapAndSerde[S, Typeclass, T]) => Typeclass[T] => Typeclass[S] +) { + def tc(using t: Typeclass[T]): Typeclass[S] = fn(wrapAndSerde, unwrapAndSerde)(t) +} + +case class WrapAndSerde[S <: AnyVal, Typeclass[_], T: Typeclass](fn: T => S) + +case class UnwrapAndSerde[S <: AnyVal, Typeclass[_], T: Typeclass](fn: S => T) diff --git a/examples/src/main/scala/magnolia1/examples/show_paramaterised.scala b/examples/src/main/scala/magnolia1/examples/show_paramaterised.scala new file mode 100644 index 00000000..0f36e9d7 --- /dev/null +++ b/examples/src/main/scala/magnolia1/examples/show_paramaterised.scala @@ -0,0 +1,110 @@ +package magnolia1.examples + +import magnolia1._ +import magnolia1.parameterised._ + +/** shows one type as another, often as a string + * + * 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 ParamaterisedShow[Out, T] extends Serializable { + def show(value: T): Out + def contramap[S](fn: S => T): ParamaterisedShow[Out, S] = (value: S) => show(fn(value)) +} + +trait ShowConfig[T] { + def hiddenFields: Set[String] +} +object ShowConfig { + given default[T]: ShowConfig[T] = new ShowConfig[T] { + override def hiddenFields: Set[String] = Set.empty + } +} + +trait ParamaterisedGenericShow[Out] extends ParamaterisedAutoDerivation[[X] =>> ParamaterisedShow[Out, X], ShowConfig] { + + def joinElems(typeName: String, strings: Seq[String]): Out + def prefix(s: String, out: Out): Out + + /** creates a new [[Show]] instance by labelling and joining (with `mkString`) the result of showing each parameter, and prefixing it with + * the class name + */ + def join[T: ShowConfig](ctx: CaseClass[Typeclass, T]): ParamaterisedShow[Out, T] = { value => + if ctx.isValueClass then + val param = ctx.params.head + param.typeclass.show(param.deref(value)) + else + val paramStrings = ctx.params.map { param => + val attribStr = + if (param.annotations.isEmpty && param.inheritedAnnotations.isEmpty) + "" + else { + (param.annotations.map(_.toString) ++ param.inheritedAnnotations.map(a => s"[i]$a")).distinct + .mkString("{", ",", "}") + } + + val tpeAttribStr = + if (param.typeAnnotations.isEmpty) "" + else { + param.typeAnnotations.mkString("{", ",", "}") + } + + s"${param.label}$attribStr$tpeAttribStr=${param.typeclass.show(param.deref(value))}" + } + + val anns = (ctx.annotations ++ ctx.inheritedAnnotations).distinct + val annotationStr = if (anns.isEmpty) "" else anns.mkString("{", ",", "}") + + val tpeAnns = ctx.typeAnnotations + val typeAnnotationStr = + if (tpeAnns.isEmpty) "" else tpeAnns.mkString("{", ",", "}") + + def typeArgsString(typeInfo: TypeInfo): String = + if typeInfo.typeParams.isEmpty then "" + else + typeInfo.typeParams + .map(arg => s"${arg.short}${typeArgsString(arg)}") + .mkString("[", ",", "]") + + joinElems( + ctx.typeInfo.short + typeArgsString( + ctx.typeInfo + ) + annotationStr + typeAnnotationStr, + paramStrings + ) + } + + /** choose which typeclass to use based on the subtype of the sealed trait and prefix with the annotations as discovered on the subtype. + */ + override def split[T: ShowConfig](ctx: SealedTrait[Typeclass, T]): ParamaterisedShow[Out, T] = + (value: T) => + ctx.choose(value) { sub => + val anns = (sub.annotations ++ sub.inheritedAnnotations).distinct + + val annotationStr = + if (anns.isEmpty) "" else anns.mkString("{", ",", "}") + + prefix(annotationStr, sub.typeclass.show(sub.value)) + } +} + +/** companion object to [[Show]] */ +object ParamaterisedShow extends ParamaterisedGenericShow[String]: + + def prefix(s: String, out: String): String = s + out + def joinElems(typeName: String, params: Seq[String]): String = + params.mkString(s"$typeName(", ",", ")") + + given ParamaterisedShow[String, String] = identity(_) + given ParamaterisedShow[String, Int] = _.toString + given ParamaterisedShow[String, Long] = _.toString + "L" + given ParamaterisedShow[String, Boolean] = _.toString + given [A](using A: ParamaterisedShow[String, A]): ParamaterisedShow[String, Seq[A]] = + _.iterator.map(A.show).mkString("[", ",", "]") + + override def handleAnyVal: [T <: AnyVal, S] => ( + WrapAndSerde[T, ParamaterisedShow.Typeclass, S], + UnwrapAndSerde[T, ParamaterisedShow.Typeclass, S] + ) => ParamaterisedShow.Typeclass[S] => ParamaterisedShow.Typeclass[T] = [T <: AnyVal, S] => + (_: WrapAndSerde[T, ParamaterisedShow.Typeclass, S], u: UnwrapAndSerde[T, ParamaterisedShow.Typeclass, S]) => + (_: ParamaterisedShow.Typeclass[S]).contramap(u.fn) diff --git a/test/src/test/scala/magnolia1/tests/ValueClassesTests.scala b/test/src/test/scala/magnolia1/tests/ValueClassesTests.scala index 29b0d051..ff569b2e 100644 --- a/test/src/test/scala/magnolia1/tests/ValueClassesTests.scala +++ b/test/src/test/scala/magnolia1/tests/ValueClassesTests.scala @@ -2,64 +2,62 @@ package magnolia1.tests import magnolia1.* import magnolia1.examples.* +//import magnolia1.tests.SerializationTests.Color /** TODO: Support for value classes is missing for scala3 branch. Eventually refactor and uncomment the tests below once the feature is * implemented. */ class ValueClassesTests extends munit.FunSuite: + import ValueClassesTests.given import ValueClassesTests.* - // test("serialize a value class") { - // val res = Show.derived[Length].show(new Length(100)) - // assertEquals(res, "100") - // } - - // test("construct a Show instance for value case class") { - // val res = Show.derived[ServiceName1].show(ServiceName1("service")) - // assertEquals(res, "service") - // } - - // test("read-only typeclass can serialize value case class with inaccessible private constructor") { - // val res = implicitly[Print[PrivateValueClass]].print(PrivateValueClass(42)) - // assertEquals(res, "42") - // } - - // test("not assume full auto derivation of external value classes") { - // val error = compileErrors(""" - // case class LoggingConfig(n: ServiceName1) - // object LoggingConfig { - // implicit val semi: SemiDefault[LoggingConfig] = SemiDefault.gen - // } - // """) - // assert(error contains """ - // |magnolia: could not find SemiDefault.Typeclass for type magnolia1.tests.ServiceName1 - // | in parameter 'n' of product type LoggingConfig - // |""".stripMargin) - // } + test("serialize a value class") { + val res = ParamaterisedShow.derived[Length].show(new Length(100)) + assertEquals(res, "100") + } - // test("serialize value case class with accessible private constructor") { - // class PrivateValueClass private (val value: Int) extends AnyVal - // object PrivateValueClass { - // def apply(l: Int) = new PrivateValueClass(l) - // implicit val show: Show[String, PrivateValueClass] = Show.derived[PrivateValueClass] - // } - // val res = PrivateValueClass.show.show(PrivateValueClass(42)) - // assertEquals(res, "42") - // } + test("construct a Show instance for value case class") { + val res = ParamaterisedShow.derived[ServiceName1].show(ServiceName1("service")) + assertEquals(res, "service") + } - // test("allow derivation result to have arbitrary type") { - // val res = (ExportedTypeclass.derived[Length], ExportedTypeclass.derived[Color]) - // assertEquals(res, (ExportedTypeclass.Exported[Length](), ExportedTypeclass.Exported[Color]())) - // } +// test("read-only typeclass can serialize value case class with inaccessible private constructor") { +// val res = implicitly[ParamaterisedGenericShow[PrivateValueClass]].show(PrivateValueClass(42)) +// assertEquals(res, "42") +// } + +// test("not assume full auto derivation of external value classes") { +// val error = compileErrors(""" +// case class LoggingConfig(n: ServiceName1) +// object LoggingConfig { +// implicit val semi: SemiDefault[LoggingConfig] = SemiDefault.gen +// } +// """) +// println(s"error = ${error}") +// assert(error contains """ +// |magnolia: could not find SemiDefault.Typeclass for type magnolia1.tests.ServiceName1 +// | in parameter 'n' of product type LoggingConfig +// |""".stripMargin) +// } + +// test("serialize value case class with accessible private constructor") { +// val res = PrivateValueClass.show.show(PrivateValueClass(42)) +// assertEquals(res, "42") +// } + +// test("allow derivation result to have arbitrary type") { +// val res = (ExportedTypeclass.derived[Length], ExportedTypeclass.derived[Color]) +// assertEquals(res, (ExportedTypeclass.Exported[Length](), ExportedTypeclass.Exported[Color]())) +// } object ValueClassesTests: - class Length(val value: Int) extends AnyVal + case class Length(val value: Int) extends AnyVal final case class ServiceName1(value: String) extends AnyVal - class PrivateValueClass private (val value: Int) extends AnyVal + case class PrivateValueClass private (val value: Int) extends AnyVal object PrivateValueClass { def apply(l: Int) = new PrivateValueClass(l) - // given Show[String, PrivateValueClass] = Show.derived + given ParamaterisedShow[String, PrivateValueClass] = ParamaterisedShow.derived }