diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/Transformer.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/Transformer.scala index b526dbee..2e5970c1 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/Transformer.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/Transformer.scala @@ -3,6 +3,7 @@ package io.github.arainko.ducktape import io.github.arainko.ducktape.builder.* import io.github.arainko.ducktape.internal.macros.* +import scala.annotation.implicitNotFound import scala.collection.Factory import scala.deriving.Mirror diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/macros/CoproductTransformations.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/macros/CoproductTransformations.scala index b2ea7ec1..437e1f90 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/macros/CoproductTransformations.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/macros/CoproductTransformations.scala @@ -19,7 +19,7 @@ private[ducktape] object CoproductTransformations { given Cases.Source = Cases.Source.fromMirror(Source) given Cases.Dest = Cases.Dest.fromMirror(Dest) - val ifBranches = singletonIfBranches[Source, Dest](sourceValue, Cases.source.value) + val ifBranches = coproductBranches[Source, Dest](sourceValue, Cases.source.value) ifStatement(ifBranches).asExprOf[Dest] } @@ -42,7 +42,7 @@ private[ducktape] object CoproductTransformations { val (nonConfiguredCases, configuredCases) = Cases.source.value.partition(c => !materializedConfig.contains(c.tpe.fullName)) - val nonConfiguredIfBranches = singletonIfBranches[Source, Dest](sourceValue, nonConfiguredCases) + val nonConfiguredIfBranches = coproductBranches[Source, Dest](sourceValue, nonConfiguredCases) val configuredIfBranches = configuredCases @@ -50,33 +50,26 @@ private[ducktape] object CoproductTransformations { .toMap .map { case (fullName, source) => - ConfiguredCase(materializedConfig(fullName), source) - } - .map { - case ConfiguredCase(config, source) => - config match { + materializedConfig(fullName) match { case Coproduct.Computed(tpe, function) => - val cond = source.tpe match { - case '[tpe] => '{ $sourceValue.isInstanceOf[tpe] } - } - val castedSource = tpe match { - case '[tpe] => '{ $sourceValue.asInstanceOf[tpe] } + val value = tpe match { + case '[tpe] => + '{ + val casted = $sourceValue.asInstanceOf[tpe] + $function(casted) + } } - val value = '{ $function($castedSource) } - cond.asTerm -> value.asTerm + IfBranch(IsInstanceOf(sourceValue, source.tpe), value) case Coproduct.Const(tpe, value) => - val cond = source.tpe match { - case '[tpe] => '{ $sourceValue.isInstanceOf[tpe] } - } - cond.asTerm -> value.asTerm + IfBranch(IsInstanceOf(sourceValue, source.tpe), value) } } ifStatement(nonConfiguredIfBranches ++ configuredIfBranches).asExprOf[Dest] } - private def singletonIfBranches[Source: Type, Dest: Type]( + private def coproductBranches[Source: Type, Dest: Type]( sourceValue: Expr[Source], sourceCases: List[Case] )(using Quotes, Cases.Dest) = { @@ -87,26 +80,44 @@ private[ducktape] object CoproductTransformations { .get(source.name) .getOrElse(Failure.emit(Failure.NoChildMapping(source.name, summon[Type[Dest]]))) }.map { (source, dest) => - val cond = source.tpe match { - case '[tpe] => '{ $sourceValue.isInstanceOf[tpe] } - } + val cond = IsInstanceOf(sourceValue, source.tpe) + + (source.tpe -> dest.tpe) match { + case '[src] -> '[dest] => + val value = + source.transformerTo(dest).map { + case '{ $t: Transformer[src, dest] } => + '{ + val castedSource = $sourceValue.asInstanceOf[src] + ${ LiftTransformation.liftTransformation(t, 'castedSource) } + } + } match { + case Right(value) => value + case Left(explanation) => + dest.materializeSingleton + .getOrElse(Failure.emit(Failure.CannotTransformCoproductCase(source.tpe, dest.tpe, explanation))) + } - cond.asTerm -> - dest.materializeSingleton - .getOrElse(Failure.emit(Failure.CannotMaterializeSingleton(dest.tpe))) + IfBranch(cond, value) + } } } - private def ifStatement(using Quotes)(branches: List[(quotes.reflect.Term, quotes.reflect.Term)]): quotes.reflect.Term = { + private def ifStatement(using Quotes)(branches: List[IfBranch]): quotes.reflect.Term = { import quotes.reflect.* branches match { - case (p1, a1) :: xs => - If(p1, a1, ifStatement(xs)) + case IfBranch(cond, value) :: xs => + If(cond.asTerm, value.asTerm, ifStatement(xs)) case Nil => '{ throw RuntimeException("Unhandled condition encountered during Coproduct Transformer derivation") }.asTerm } } - private case class ConfiguredCase(config: Coproduct, subcase: Case) + private def IsInstanceOf(value: Expr[Any], tpe: Type[?])(using Quotes) = + tpe match { + case '[tpe] => '{ $value.isInstanceOf[tpe] } + } + + private case class IfBranch(cond: Expr[Boolean], value: Expr[Any]) } diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/modules/Case.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/modules/Case.scala index 501fe4e0..6a738714 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/modules/Case.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/modules/Case.scala @@ -1,19 +1,33 @@ package io.github.arainko.ducktape.internal.modules +import io.github.arainko.ducktape.Transformer + import scala.quoted.* private[ducktape] final case class Case( val name: String, - val tpe: Type[?], - val ordinal: Int + val tpe: Type[?] ) { - def materializeSingleton(using Quotes): Option[quotes.reflect.Term] = { + + def transformerTo(that: Case)(using Quotes): Either[String, Expr[Transformer[?, ?]]] = { + import quotes.reflect.* + + (tpe -> that.tpe) match { + case '[src] -> '[dest] => + Implicits.search(TypeRepr.of[Transformer[src, dest]]) match { + case success: ImplicitSearchSuccess => Right(success.tree.asExprOf[Transformer[src, dest]]) + case err: ImplicitSearchFailure => Left(err.explanation) + } + } + } + + def materializeSingleton(using Quotes): Option[Expr[Any]] = { import quotes.reflect.* val typeRepr = TypeRepr.of(using tpe) Option.when(typeRepr.isSingleton) { - typeRepr match { case TermRef(a, b) => Ident(TermRef(a, b)) } + typeRepr match { case ref: TermRef => Ident(ref).asExpr } } } } diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/modules/Cases.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/modules/Cases.scala index fb38c7b6..94b2cd0a 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/modules/Cases.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/modules/Cases.scala @@ -11,7 +11,7 @@ private[ducktape] sealed trait Cases { val byName: Map[String, Case] = value.map(c => c.name -> c).toMap } -object Cases { +private[ducktape] object Cases { def source(using sourceCases: Cases.Source): Cases.Source = sourceCases def dest(using destCases: Cases.Dest): Cases.Dest = destCases @@ -25,12 +25,11 @@ object Cases { def apply(cases: List[Case]): CasesSubtype final def fromMirror[A: Type](mirror: Expr[Mirror.SumOf[A]])(using Quotes): CasesSubtype = { - val materializedMirror = MaterializedMirror.createOrAbort(mirror) + val materializedMirror = MaterializedMirror.create(mirror) val cases = materializedMirror.mirroredElemLabels .zip(materializedMirror.mirroredElemTypes) - .zipWithIndex - .map { case name -> tpe -> ordinal => Case(name, tpe.asType, ordinal) } + .map(Case.apply) apply(cases) } diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/modules/Constructor.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/modules/Constructor.scala index 019dc084..86bfd4a6 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/modules/Constructor.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/modules/Constructor.scala @@ -12,6 +12,9 @@ private[ducktape] object Constructor { case notApplied => (tpe, tpe.typeSymbol.primaryConstructor, Nil) } + // workaround for invoking constructors of singleton which in turn actually create new instances of singletons! + if (tpe.typeSymbol.flags.is(Flags.Module)) report.errorAndAbort("Cannot invoke constructor of a singleton") + New(Inferred(repr)) .select(constructor) .appliedToTypes(tpeArgs) diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/modules/Failure.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/modules/Failure.scala index 3513eb51..f7a901ba 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/modules/Failure.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/modules/Failure.scala @@ -142,13 +142,15 @@ private[ducktape] object Failure { override final def render(using Quotes): String = s"No child named '$childName' found in ${destinationType.show}" } - final case class CannotMaterializeSingleton(tpe: Type[?]) extends Failure { - private def suggestions(using Quotes) = Suggestion.all(s"${tpe.show} is not a singleton type") - + final case class CannotTransformCoproductCase(source: Type[?], dest: Type[?], implicitSearchExplanation: String) + extends Failure { override final def render(using Quotes): String = s""" - |Cannot materialize singleton for ${tpe.show}. - |Possible causes: ${Suggestion.renderAll(suggestions)} + |Neither an instance of Transformer[${source.fullName}, ${dest.fullName}] was found nor are '${source.show}' '${dest.show}' + |singletons with the same name. + | + |Compiler supplied explanation for the failed Transformer derivation (may or may not be helpful): + |$implicitSearchExplanation """.stripMargin } diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/modules/Field.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/modules/Field.scala index 67e7d3d4..506308a9 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/modules/Field.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/modules/Field.scala @@ -5,7 +5,10 @@ import io.github.arainko.ducktape.fallible.FallibleTransformer import scala.quoted.* -private[ducktape] final class Field(val name: String, val tpe: Type[?], val default: Option[Expr[Any]]) { +private[ducktape] final class Field(val name: String, val tpe: Type[?], defaultValue: => Option[Expr[Any]]) { + + lazy val default: Option[Expr[Any]] = defaultValue + def transformerTo(that: Field)(using Quotes): Expr[Transformer[?, ?]] = { import quotes.reflect.* diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/modules/Fields.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/modules/Fields.scala index 1aae30a3..cc11356b 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/modules/Fields.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/modules/Fields.scala @@ -28,12 +28,14 @@ private[ducktape] object Fields { def apply(fields: List[Field]): FieldsSubtype final def fromMirror[A: Type](mirror: Expr[Mirror.ProductOf[A]])(using Quotes): FieldsSubtype = { - val materializedMirror = MaterializedMirror.createOrAbort(mirror) + val materializedMirror = MaterializedMirror.create(mirror) + + lazy val defaults = defaultParams[A] - val defaults = defaultParams[A] val fields = materializedMirror.mirroredElemLabels .zip(materializedMirror.mirroredElemTypes) - .map((name, tpe) => Field(name, tpe.asType, defaults.get(name))) + .map((name, tpe) => Field(name, tpe, defaults.get(name))) + apply(fields) } @@ -53,21 +55,21 @@ private[ducktape] object Fields { apply(fields) } - private def defaultParams[T: Type](using Quotes): Map[String, Expr[Any]] = { + private def defaultParams[A: Type](using Quotes): Map[String, Expr[Any]] = { import quotes.reflect.* - val typ = TypeRepr.of[T] - val sym = typ.typeSymbol - val typeArgs = typ.typeArgs - val mod = Ref(sym.companionModule) - val names = sym.caseFields.filter(_.flags.is(Flags.HasDefault)).map(_.name) - val body = sym.companionClass.tree.asInstanceOf[ClassDef].body - val idents: List[Term] = body.collect { + val tpe = TypeRepr.of[A] + val sym = tpe.typeSymbol + val typeArgs = tpe.typeArgs + val companion = Ref(sym.companionModule) + val fieldNamesWithDefaults = sym.caseFields.filter(_.flags.is(Flags.HasDefault)).map(_.name) + val companionBody = sym.companionClass.tree.asInstanceOf[ClassDef].body + val defaultValues = companionBody.collect { case deff @ DefDef(name, _, _, _) if name.startsWith("$lessinit$greater$default") => - mod.select(deff.symbol).appliedToTypes(typeArgs) + companion.select(deff.symbol).appliedToTypes(typeArgs).asExpr } - names.zip(idents.map(_.asExpr)).toMap + fieldNamesWithDefaults.zip(defaultValues).toMap } } } diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/modules/MaterializedMirror.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/modules/MaterializedMirror.scala index 36b231d5..6bb45ac4 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/modules/MaterializedMirror.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/modules/MaterializedMirror.scala @@ -4,67 +4,40 @@ import scala.annotation.tailrec import scala.deriving.Mirror import scala.quoted.* -private[ducktape] final class MaterializedMirror[Q <: Quotes & Singleton] private (using val quotes: Q)( - val mirroredType: quotes.reflect.TypeRepr, - val mirroredMonoType: quotes.reflect.TypeRepr, - val mirroredElemTypes: List[quotes.reflect.TypeRepr], - val mirroredLabel: String, +private[ducktape] final class MaterializedMirror( + val mirroredElemTypes: List[Type[?]], val mirroredElemLabels: List[String] ) -// Lifted from shapeless 3: -// https://github.com/typelevel/shapeless-3/blob/main/modules/deriving/src/main/scala/shapeless3/deriving/internals/reflectionutils.scala private[ducktape] object MaterializedMirror { - def createOrAbort[A: Type](mirror: Expr[Mirror.Of[A]])(using Quotes): MaterializedMirror[quotes.type] = - create(mirror).fold(memberName => Failure.emit(Failure.MirrorMaterialization(summon, memberName)), identity) - - private def create(mirror: Expr[Mirror])(using Quotes): Either[String, MaterializedMirror[quotes.type]] = { - import quotes.reflect.* - - val mirrorTpe = mirror.asTerm.tpe.widen - for { - mirroredType <- findMemberType(mirrorTpe, "MirroredType") - mirroredMonoType <- findMemberType(mirrorTpe, "MirroredMonoType") - mirroredElemTypes <- findMemberType(mirrorTpe, "MirroredElemTypes") - mirroredLabel <- findMemberType(mirrorTpe, "MirroredLabel") - mirroredElemLabels <- findMemberType(mirrorTpe, "MirroredElemLabels") - } yield { - val elemTypes = tupleTypeElements(mirroredElemTypes) - val ConstantType(StringConstant(label)) = mirroredLabel: @unchecked - val elemLabels = tupleTypeElements(mirroredElemLabels).map { case ConstantType(StringConstant(l)) => l } - MaterializedMirror(mirroredType, mirroredMonoType, elemTypes, label, elemLabels) + def create[A: Type](mirror: Expr[Mirror.Of[A]])(using Quotes): MaterializedMirror = + mirror match { + case '{ + $m: Mirror.Of[A] { + type MirroredElemTypes = types + type MirroredElemLabels = labels + } + } => + import quotes.reflect.* + val elemTypes = tupleTypeElements(TypeRepr.of[types]) + val labels = tupleTypeElements(TypeRepr.of[labels]).map { + case '[IsString[tpe]] => Type.valueOfConstant[tpe].getOrElse(report.errorAndAbort("Couldn't extract constact value")) + } + MaterializedMirror(elemTypes, labels) } - } - private def tupleTypeElements(using Quotes)(tp: quotes.reflect.TypeRepr): List[quotes.reflect.TypeRepr] = { + private def tupleTypeElements(using Quotes)(tp: quotes.reflect.TypeRepr): List[Type[?]] = { import quotes.reflect.* - @tailrec def loop(tp: TypeRepr, acc: List[TypeRepr]): List[TypeRepr] = tp match { - case AppliedType(pairTpe, List(hd: TypeRepr, tl: TypeRepr)) => loop(tl, hd :: acc) - case _ => acc - } + @tailrec def loop(curr: TypeRepr, acc: List[Type[?]]): List[Type[?]] = + curr match { + case AppliedType(pairTpe, head :: tail :: Nil) => loop(tail, head.asType :: acc) + case _ => acc + } loop(tp, Nil).reverse } - private def low(using Quotes)(tp: quotes.reflect.TypeRepr): quotes.reflect.TypeRepr = { - import quotes.reflect.* - - tp match { - case tp: TypeBounds => tp.low - case tp => tp - } - } - - private def findMemberType(using Quotes)(tp: quotes.reflect.TypeRepr, name: String): Either[String, quotes.reflect.TypeRepr] = { - import quotes.reflect.* - - tp match { - case Refinement(_, `name`, tp) => Right(low(tp)) - case Refinement(parent, _, _) => findMemberType(parent, name) - case AndType(left, right) => findMemberType(left, name).orElse(findMemberType(right, name)) - case _ => Left(name) - } - } + private type IsString[A <: String] = A } diff --git a/ducktape/src/test/scala/io/github/arainko/ducktape/total/builder/AppliedBuilderSuite.scala b/ducktape/src/test/scala/io/github/arainko/ducktape/total/builder/AppliedBuilderSuite.scala index 89a92b2f..89760710 100644 --- a/ducktape/src/test/scala/io/github/arainko/ducktape/total/builder/AppliedBuilderSuite.scala +++ b/ducktape/src/test/scala/io/github/arainko/ducktape/total/builder/AppliedBuilderSuite.scala @@ -208,6 +208,69 @@ class AppliedBuilderSuite extends DucktapeSuite { assertEquals(actual(NotEnumMoreCases.Case4(2)), expectedForOther) } + test("sums of products can be configured") { + enum Sum1 { + case Leaf1(int: Int, str: String) + case Leaf2(int1: Int, str2: String, list: List[Int]) + case Leaf3(int3: Int, str3: String, opt: Option[Int]) + case Singleton + } + + enum Sum2 { + case Leaf1(int: Int, str: String) + case Leaf3(int3: Int, str3: String, opt: Option[Int]) + case Singleton2 + } + + val transformer = + Transformer + .define[Sum1, Sum2] + .build( + Case.computed[Sum1.Leaf2](leaf2 => Sum2.Leaf1(leaf2.int1, leaf2.str2)), + Case.const[Sum1.Singleton.type](Sum2.Singleton2) + ) + + val expectedMappings = + Map( + Sum1.Leaf1(1, "str") -> Sum2.Leaf1(1, "str"), + Sum1.Leaf2(2, "str2", List(1, 2, 3)) -> Sum2.Leaf1(2, "str2"), + Sum1.Leaf3(3, "str3", Some(1)) -> Sum2.Leaf3(3, "str3", Some(1)), + Sum1.Singleton -> Sum2.Singleton2 + ) + + expectedMappings.foreach { (sum1, expected) => + val actual = sum1 + .into[Sum2] + .transform( + Case.computed[Sum1.Leaf2](leaf2 => Sum2.Leaf1(leaf2.int1, leaf2.str2)), + Case.const[Sum1.Singleton.type](Sum2.Singleton2) + ) + assertEquals(actual, expected) + } + } + + test("sums with type parameters can be confgured") { + enum Sum1[A] { + case Leaf1(int: Int, a: A) + } + + enum Sum2[A] { + case Leaf2(int: Int, a: Option[A]) + } + + val leaf1 = Sum1.Leaf1(1, 1) + + val expected = Sum2.Leaf2(1, Some("1")) + + val actual = leaf1 + .into[Sum2[String]] + .transform( + Case.computed[Sum1.Leaf1[Int]](a => Sum2.Leaf2(a.int, Some(a.a.toString()))) + ) + + assertEquals(actual, expected) + } + test("When a Case is configured multiple times a warning is emitted") { assertFailsToCompileWith { diff --git a/ducktape/src/test/scala/io/github/arainko/ducktape/total/builder/DefinitionViaBuilderSuite.scala b/ducktape/src/test/scala/io/github/arainko/ducktape/total/builder/DefinitionViaBuilderSuite.scala index a1681132..74df2c6b 100644 --- a/ducktape/src/test/scala/io/github/arainko/ducktape/total/builder/DefinitionViaBuilderSuite.scala +++ b/ducktape/src/test/scala/io/github/arainko/ducktape/total/builder/DefinitionViaBuilderSuite.scala @@ -47,6 +47,41 @@ class DefinitionViaBuilderSuite extends DucktapeSuite { assertEquals(transformer.transform(testClass), expected) } + test("sums of products can be configured") { + enum Sum1 { + case Leaf1(int: Int, str: String) + case Leaf2(int1: Int, str2: String, list: List[Int]) + case Leaf3(int3: Int, str3: String, opt: Option[Int]) + case Singleton + } + + enum Sum2 { + case Leaf1(int: Int, str: String) + case Leaf3(int3: Int, str3: String, opt: Option[Int]) + case Singleton2 + } + + val transformer = + Transformer + .define[Sum1, Sum2] + .build( + Case.computed[Sum1.Leaf2](leaf2 => Sum2.Leaf1(leaf2.int1, leaf2.str2)), + Case.const[Sum1.Singleton.type](Sum2.Singleton2) + ) + + val expectedMappings = + Map( + Sum1.Leaf1(1, "str") -> Sum2.Leaf1(1, "str"), + Sum1.Leaf2(2, "str2", List(1, 2, 3)) -> Sum2.Leaf1(2, "str2"), + Sum1.Leaf3(3, "str3", Some(1)) -> Sum2.Leaf3(3, "str3", Some(1)), + Sum1.Singleton -> Sum2.Singleton2 + ) + + expectedMappings.foreach { (sum1, expected) => + assertEquals(transformer.transform(sum1), expected) + } + } + test("Builder reports a missing argument") { assertFailsToCompileWith { """ diff --git a/ducktape/src/test/scala/io/github/arainko/ducktape/total/derivation/DerivedTransformerSuite.scala b/ducktape/src/test/scala/io/github/arainko/ducktape/total/derivation/DerivedTransformerSuite.scala index 1e37a1da..7aaae190 100644 --- a/ducktape/src/test/scala/io/github/arainko/ducktape/total/derivation/DerivedTransformerSuite.scala +++ b/ducktape/src/test/scala/io/github/arainko/ducktape/total/derivation/DerivedTransformerSuite.scala @@ -260,6 +260,96 @@ class DerivedTransformerSuite extends DucktapeSuite { assertEquals(actual, expected) } + test("derivation succeeds when going from a sum of cases with the same name as the target sum (enum)") { + + enum Sum1 { + case Leaf1(int: Int, str: String) + case Leaf2(int1: Int, str2: String, list: List[Int]) + case Leaf3(int3: Int, str3: String, opt: Option[Int], nested: Nested1) + case Singleton + } + + enum Sum2 { + case Leaf1(int: Int | Double, str: CharSequence) + case Leaf2(int1: Int | Long, str2: CharSequence, list: Vector[Int | String]) + case Leaf3(int3: Int, str3: String, opt: Option[Int], nested: Nested2) + case Singleton + } + + case class Nested1(int: Int) + case class Nested2(int: Int) + + val expectedMappings = + Map( + Sum1.Leaf1(1, "str") -> Sum2.Leaf1(1, "str"), + Sum1.Leaf2(2, "str2", List(1, 2, 3)) -> Sum2.Leaf2(2, "str2", Vector(1, 2, 3)), + Sum1.Leaf3(3, "str3", None, Nested1(1)) -> Sum2.Leaf3(3, "str3", None, Nested2(1)), + Sum1.Singleton -> Sum2.Singleton + ) + + expectedMappings.foreach((sum1, expected) => assertEquals(sum1.to[Sum2], expected)) + } + + test("derivation succeeds when going from a sum of cases with the same name as the target sum (sealed trait)") { + sealed trait Sum1 + + object Sum1 { + case class Leaf1(int: Int, str: String) extends Sum1 + case class Leaf2(int1: Int, str2: String, list: List[Int]) extends Sum1 + case class Leaf3(int3: Int, str3: String, opt: Option[Int]) extends Sum1 + case object Singleton extends Sum1 + } + + sealed trait Sum2 + + object Sum2 { + case class Leaf1(int: Int, str: String) extends Sum2 + case class Leaf2(int1: Int, str2: String, list: Vector[Int | String]) extends Sum2 + case class Leaf3(int3: Int, str3: String, opt: Option[Int]) extends Sum2 + case object Singleton extends Sum2 + } + + val expectedMappings = + Map( + Sum1.Leaf1(1, "str") -> Sum2.Leaf1(1, "str"), + Sum1.Leaf2(2, "str2", List(1, 2, 3)) -> Sum2.Leaf2(2, "str2", Vector(1, 2, 3)), + Sum1.Leaf3(3, "str3", Some(1)) -> Sum2.Leaf3(3, "str3", Some(1)), + Sum1.Singleton -> Sum2.Singleton + ) + + expectedMappings.foreach((sum1, expected) => assertEquals(sum1.to[Sum2], expected)) + } + + test("derivation succeeds betweens sums with type parameters") { + enum Sum1[A] { + case Leaf1(int: Int, a: A) + } + + enum Sum2[A] { + case Leaf1(int: Int, a: Option[A]) + } + + val leaf1 = Sum1.Leaf1(1, "asd") + val expected = Sum2.Leaf1(1, Some("asd")) + + assertEquals(leaf1.to[Sum2[String]], expected) + } + + test("derivation fails when a Transformer doesn't exist for a child with the same name") { + enum Sum1 { + case Leaf1(int: Int, str: String) + } + + enum Sum2 { + case Leaf1(str1: String) + } + + assertFailsToCompileWith("summon[Transformer[Sum1, Sum2]]") { + """Neither an instance of Transformer[Sum1.Leaf1, Sum2.Leaf1] was found nor are 'Leaf1' 'Leaf1' +singletons with the same name""" + } + } + test("derivation fails when going from a sum with more cases to a sum with less cases") { assertFailsToCompileWith("MoreCases.Case3.to[LessCases]")("No child named 'Case4' found in LessCases") }