From a53dc9a57198217c68a2ada0009053981b722a8e Mon Sep 17 00:00:00 2001 From: Aleksander Rainko Date: Thu, 25 Jul 2024 06:23:44 +0200 Subject: [PATCH 1/4] repro --- .../internal/ConfigInstructionRefiner.scala | 15 +++++++++++++++ .../github/arainko/ducktape/internal/Logger.scala | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/ConfigInstructionRefiner.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/ConfigInstructionRefiner.scala index aac48a1d..3eb38334 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/ConfigInstructionRefiner.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/ConfigInstructionRefiner.scala @@ -1,6 +1,7 @@ package io.github.arainko.ducktape.internal import io.github.arainko.ducktape.internal.Configuration.* +import io.github.arainko.ducktape.* private[ducktape] object ConfigInstructionRefiner { @@ -13,3 +14,17 @@ private[ducktape] object ConfigInstructionRefiner { case inst: (Instruction.Dynamic | Instruction.Bulk | Instruction.Regional | Instruction.Failed) => inst } + +object Common { + given Transformer.Fallible[Option, Int, String] = a => Some(a.toString) +} + +case class Costam1(int: Int) +case class Costam2(int: Option[String]) + +object test { + import Common.given + Mode.FailFast.option.locally { + Costam1(1).fallibleTo[Costam2] + } +} diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Logger.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Logger.scala index 9d45a5af..9e978d62 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Logger.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Logger.scala @@ -6,7 +6,7 @@ import scala.quoted.* private[ducktape] object Logger { // Logger Config - private[ducktape] transparent inline given level: Level = Level.Off + private[ducktape] transparent inline given level: Level = Level.Debug private val output = Output.StdOut private def filter(msg: String, loc: String) = true enum Level { From 047709865f8c96a67bead725f9b9ee8ec33ccffd Mon Sep 17 00:00:00 2001 From: Aleksander Rainko Date: Sat, 27 Jul 2024 11:52:12 +0200 Subject: [PATCH 2/4] resolve Option and Iterable structures BEFORE resolving Wrappeds --- .../github/arainko/ducktape/internal/Structure.scala | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Structure.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Structure.scala index eb5f4e61..b2c6722e 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Structure.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Structure.scala @@ -95,6 +95,12 @@ private[ducktape] object Structure { case tpe @ '[Nothing] => Structure.Ordinary(tpe, path) + case tpe @ '[Option[param]] => + Structure.Optional(tpe, path, Structure.of[param](path.appended(Path.Segment.Element(Type.of[param])))) + + case tpe @ '[Iterable[param]] => + Structure.Collection(tpe, path, Structure.of[param](path.appended(Path.Segment.Element(Type.of[param])))) + case WrapperType(wrapper: WrapperType.Wrapped[f], '[underlying]) => @unused given Type[f] = wrapper.wrapperTpe Structure.Wrappped( @@ -103,12 +109,6 @@ private[ducktape] object Structure { Structure.of[underlying](path.appended(Path.Segment.Element(Type.of[underlying]))) ) - case tpe @ '[Option[param]] => - Structure.Optional(tpe, path, Structure.of[param](path.appended(Path.Segment.Element(Type.of[param])))) - - case tpe @ '[Iterable[param]] => - Structure.Collection(tpe, path, Structure.of[param](path.appended(Path.Segment.Element(Type.of[param])))) - case tpe @ '[AnyVal] if tpe.repr.typeSymbol.flags.is(Flags.Case) => val repr = tpe.repr val param = repr.typeSymbol.caseFields.head From dc8ef54b2e2b073d56f5873a8efff85c8ca21898 Mon Sep 17 00:00:00 2001 From: Aleksander Rainko Date: Sat, 27 Jul 2024 13:27:52 +0200 Subject: [PATCH 3/4] fix #187 and #190 --- .../arainko/ducktape/internal/Structure.scala | 28 +++++++++++-------- .../ducktape/issues/Issue187Suite.scala | 16 +++++++++++ .../ducktape/issues/Issue190Suite.scala | 19 +++++++++++++ 3 files changed, 51 insertions(+), 12 deletions(-) create mode 100644 ducktape/src/test/scala/io/github/arainko/ducktape/issues/Issue187Suite.scala create mode 100644 ducktape/src/test/scala/io/github/arainko/ducktape/issues/Issue190Suite.scala diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Structure.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Structure.scala index b2c6722e..ef3670c5 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Structure.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Structure.scala @@ -117,7 +117,7 @@ private[ducktape] object Structure { case tpe @ '[Any *: scala.Tuple] if !tpe.repr.isTupleN => // let plain tuples be caught later on val elements = - tupleTypeElements(tpe.repr.dealias).zipWithIndex.map { (tpe, idx) => + tupleTypeElements(tpe).zipWithIndex.map { (tpe, idx) => tpe.asType match { case '[tpe] => Lazy.of[tpe](path.appended(Path.Segment.TupleElement(Type.of[tpe], idx))) } @@ -155,7 +155,7 @@ private[ducktape] object Structure { } } if tpe.repr.isTupleN => val structures = - tupleTypeElements(TypeRepr.of[types]).zipWithIndex + tupleTypeElements(Type.of[types]).zipWithIndex .map((tpe, idx) => tpe.asType match { case '[tpe] => Lazy.of[tpe](path.appended(Path.Segment.TupleElement(Type.of[tpe], idx))) @@ -172,7 +172,7 @@ private[ducktape] object Structure { } } => val structures = - tupleTypeElements(TypeRepr.of[types]) + tupleTypeElements(Type.of[types]) .zip(constStringTuple(TypeRepr.of[labels])) .map((tpe, name) => name -> (tpe.asType match { @@ -189,7 +189,7 @@ private[ducktape] object Structure { } } => val structures = - tupleTypeElements(TypeRepr.of[types]) + tupleTypeElements(Type.of[types]) .zip(constStringTuple(TypeRepr.of[labels])) .map((tpe, name) => name -> (tpe.asType match { case '[tpe] => Lazy.of[tpe](path.appended(Path.Segment.Case(Type.of[tpe]))) }) @@ -209,21 +209,25 @@ private[ducktape] object Structure { private def constantString[Const <: String: Type](using Quotes) = Type.valueOfConstant[Const].get - private def tupleTypeElements(using Quotes)(tp: quotes.reflect.TypeRepr): List[quotes.reflect.TypeRepr] = { - import quotes.reflect.* + private def tupleTypeElements(tpe: Type[?])(using Quotes): List[quotes.reflect.TypeRepr] = { + @tailrec def loop(using Quotes)(curr: Type[?], acc: List[quotes.reflect.TypeRepr]): List[quotes.reflect.TypeRepr] = { + import quotes.reflect.* - @tailrec def loop(curr: TypeRepr, acc: List[TypeRepr]): List[TypeRepr] = curr match { - case AppliedType(pairTpe, head :: tail :: Nil) => - loop(tail, head :: acc) - case _ => + case '[head *: tail] => + loop(Type.of[tail], TypeRepr.of[head] :: acc) + case '[EmptyTuple] => acc + case other => + report.errorAndAbort(s"Unexpected type (${other.repr.show}) encountered when extracting tuple type elems. This is a bug in ducktape.") } - loop(tp, Nil).reverse + } + + loop(tpe, Nil).reverse } private def constStringTuple(using Quotes)(tp: quotes.reflect.TypeRepr): List[String] = { import quotes.reflect.* - tupleTypeElements(tp).map { case ConstantType(StringConstant(l)) => l } + tupleTypeElements(tp.asType).map { case ConstantType(StringConstant(l)) => l } } } diff --git a/ducktape/src/test/scala/io/github/arainko/ducktape/issues/Issue187Suite.scala b/ducktape/src/test/scala/io/github/arainko/ducktape/issues/Issue187Suite.scala new file mode 100644 index 00000000..ed9c1c64 --- /dev/null +++ b/ducktape/src/test/scala/io/github/arainko/ducktape/issues/Issue187Suite.scala @@ -0,0 +1,16 @@ +package io.github.arainko.ducktape.issues +import io.github.arainko.ducktape.* + +class Issue187Suite extends DucktapeSuite { + test("minimization works") { + + given Transformer.Fallible[Option, Int, String] = a => Some(a.toString) + + case class Source(int: Int) + case class Dest(int: Option[String]) + + Mode.FailFast.option.locally { + Source(1).fallibleTo[Dest] + } + } +} diff --git a/ducktape/src/test/scala/io/github/arainko/ducktape/issues/Issue190Suite.scala b/ducktape/src/test/scala/io/github/arainko/ducktape/issues/Issue190Suite.scala new file mode 100644 index 00000000..71f1c492 --- /dev/null +++ b/ducktape/src/test/scala/io/github/arainko/ducktape/issues/Issue190Suite.scala @@ -0,0 +1,19 @@ +package io.github.arainko.ducktape.issues + +import io.github.arainko.ducktape.* + +class Issue190Suite extends DucktapeSuite { + + test("transforming from tuples concatenated together works") { + case class Big(int1: Int, int2: Int, int3: Int, int4: Int, int5: Int, int6: Int, int7: Int, int8: Int) + + val one = (1, 2, 3, 4) + val two = (5, 6, 7, 8) + + val joined = (one ++ two) + + val expected = Big(1, 2, 3, 4, 5, 6, 7, 8) + assertTransforms(joined, expected) + } + +} From ff6b08c2ad6a5929482babbde0de03fe75f8bf4e Mon Sep 17 00:00:00 2001 From: Aleksander Rainko Date: Sun, 28 Jul 2024 21:01:18 +0200 Subject: [PATCH 4/4] make Wrapped(WrapperType.Optional) isomorphic to Optional --- .../internal/ConfigInstructionRefiner.scala | 16 +--- .../arainko/ducktape/internal/Context.scala | 2 +- .../internal/FallibleTransformations.scala | 6 +- .../arainko/ducktape/internal/Logger.scala | 2 +- .../arainko/ducktape/internal/Plan.scala | 4 +- .../arainko/ducktape/internal/Planner.scala | 33 ++++--- .../arainko/ducktape/internal/Structure.scala | 30 ++++--- .../ducktape/internal/WrapperType.scala | 44 ++++++++-- .../ducktape/issues/Issue187Suite.scala | 86 ++++++++++++++++++- .../ducktape/issues/Issue190Suite.scala | 12 ++- 10 files changed, 177 insertions(+), 58 deletions(-) diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/ConfigInstructionRefiner.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/ConfigInstructionRefiner.scala index 3eb38334..9c4ac60e 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/ConfigInstructionRefiner.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/ConfigInstructionRefiner.scala @@ -1,7 +1,7 @@ package io.github.arainko.ducktape.internal -import io.github.arainko.ducktape.internal.Configuration.* import io.github.arainko.ducktape.* +import io.github.arainko.ducktape.internal.Configuration.* private[ducktape] object ConfigInstructionRefiner { @@ -14,17 +14,3 @@ private[ducktape] object ConfigInstructionRefiner { case inst: (Instruction.Dynamic | Instruction.Bulk | Instruction.Regional | Instruction.Failed) => inst } - -object Common { - given Transformer.Fallible[Option, Int, String] = a => Some(a.toString) -} - -case class Costam1(int: Int) -case class Costam2(int: Option[String]) - -object test { - import Common.given - Mode.FailFast.option.locally { - Costam1(1).fallibleTo[Costam2] - } -} diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Context.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Context.scala index e83b6dcc..a6a1259d 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Context.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Context.scala @@ -18,7 +18,7 @@ private[ducktape] object Context { transparent inline def current(using ctx: Context): ctx.type = ctx case class PossiblyFallible[G[+x]]( - wrapperType: WrapperType.Wrapped[G], + wrapperType: WrapperType[G], transformationSite: TransformationSite, summoner: Summoner.PossiblyFallible[G], mode: TransformationMode[G] diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/FallibleTransformations.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/FallibleTransformations.scala index e2bc05b0..76e32923 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/FallibleTransformations.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/FallibleTransformations.scala @@ -19,7 +19,7 @@ private[ducktape] object FallibleTransformations { configs: Expr[Seq[Field.Fallible[F, A, B] | Case.Fallible[F, A, B]]] )(using Quotes): Expr[F[B]] = { given Context.PossiblyFallible[F]( - WrapperType.Wrapped(Type.of[F]), + WrapperType.create[F], TransformationSite.fromStringExpr(transformationSite), Summoner.PossiblyFallible[F], TransformationMode.create(F) @@ -50,7 +50,7 @@ private[ducktape] object FallibleTransformations { configs: Expr[Seq[Field.Fallible[F, A, Args] | Case.Fallible[F, A, Args]]] )(using Quotes): Expr[F[B]] = { given Context.PossiblyFallible[F]( - WrapperType.Wrapped(Type.of[F]), + WrapperType.create[F], TransformationSite.fromStringExpr(transformationSite), Summoner.PossiblyFallible[F], TransformationMode.create(F) @@ -92,7 +92,7 @@ private[ducktape] object FallibleTransformations { import quotes.reflect.* given Context.PossiblyFallible[F]( - WrapperType.Wrapped(Type.of[F]), + WrapperType.create[F], TransformationSite.Transformation, Summoner.PossiblyFallible[F], TransformationMode.create(F) diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Logger.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Logger.scala index 9e978d62..9d45a5af 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Logger.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Logger.scala @@ -6,7 +6,7 @@ import scala.quoted.* private[ducktape] object Logger { // Logger Config - private[ducktape] transparent inline given level: Level = Level.Debug + private[ducktape] transparent inline given level: Level = Level.Off private val output = Output.StdOut private def filter(msg: String, loc: String) = true enum Level { diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Plan.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Plan.scala index 63faae78..862d2b30 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Plan.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Plan.scala @@ -84,13 +84,13 @@ private[ducktape] object Plan { ) extends Plan[Nothing, Nothing] case class BetweenFallibleNonFallible[+E <: Erroneous]( - source: Structure.Wrappped[?], + source: Structure.Wrapped[?], dest: Structure, plan: Plan[E, Nothing] ) extends Plan[E, Fallible] case class BetweenFallibles[+E <: Erroneous]( - source: Structure.Wrappped[?], + source: Structure.Wrapped[?], dest: Structure, mode: TransformationMode.FailFast[?], plan: Plan[E, Fallible] diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Planner.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Planner.scala index 3ac2b3a3..90bc26a1 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Planner.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Planner.scala @@ -61,6 +61,22 @@ private[ducktape] object Planner { recurse(source, paramStruct) ) + // Wrapped(WrapperType.Optional) is isomorphic to Optional + // scalafmt: { maxColumn = 150 } + case (source @ Wrapped(_, WrapperType.Optional, _, srcUnderlying)) -> (dest @ Wrapped(_, WrapperType.Optional, _, destUnderlying)) => + Plan.BetweenOptions( + Structure.Optional.fromWrapped(source), + Structure.Optional.fromWrapped(dest), + recurse(srcUnderlying, destUnderlying) + ) + + case source -> (dest @ Wrapped(_, WrapperType.Optional, _, underlying)) => + Plan.BetweenNonOptionOption( + source, + Structure.Optional.fromWrapped(dest), + recurse(source, underlying) + ) + case (source @ Collection(_, _, srcParamStruct)) -> (dest @ Collection(_, _, destParamStruct)) => Plan.BetweenCollections( source, @@ -246,8 +262,7 @@ private[ducktape] object Planner { boundary[Plan.Error | plan.type]: var owner = Symbol.spliceOwner while (!owner.isNoSymbol) { - if owner == transformerSymbol then - boundary.break(Plan.Error.from(plan, ErrorMessage.LoopingTransformerDetected, None)) + if owner == transformerSymbol then boundary.break(Plan.Error.from(plan, ErrorMessage.LoopingTransformerDetected, None)) owner = owner.maybeOwner } plan @@ -260,7 +275,7 @@ private[ducktape] object Planner { structs: (Structure, Structure) )(using Quotes, Depth, Context.Of[F]): Option[Plan[Erroneous, F]] = PartialFunction.condOpt(Context.current *: structs) { - case (ctx: Context.PossiblyFallible[f], source @ Wrappped(tpe, path, underlying), dest) => + case (ctx: Context.PossiblyFallible[f], source @ Wrapped(tpe, _, path, underlying), dest) => // needed for the recurse call to return Plan[Erroneous, Nothing] given Context.Total = ctx.toTotal val plan = Plan.BetweenFallibleNonFallible( @@ -283,7 +298,7 @@ private[ducktape] object Planner { PartialFunction.condOpt(Context.current *: structs) { case ( ctx @ Context.PossiblyFallible(_, _, _, mode: TransformationMode.FailFast[f]), - source @ Wrappped(tpe, path, underlying), + source @ Wrapped(tpe, _, path, underlying), dest ) => ctx.reifyPlan[F] { @@ -296,13 +311,8 @@ private[ducktape] object Planner { } case ( - ctx @ Context.PossiblyFallible( - WrapperType.Wrapped(given Type[f]), - _, - _, - TransformationMode.Accumulating(mode, Some(localMode)) - ), - source @ Wrappped(tpe, path, underlying), + ctx @ Context.PossiblyFallible(_, _, _, TransformationMode.Accumulating(mode, Some(localMode))), + source @ Wrapped(tpe, _, path, underlying), dest ) => ctx.reifyPlan[F] { @@ -313,7 +323,6 @@ private[ducktape] object Planner { recurse(underlying, dest) ) } - } } } diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Structure.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Structure.scala index ef3670c5..69da0100 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Structure.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Structure.scala @@ -52,6 +52,11 @@ private[ducktape] object Structure { case class Optional(tpe: Type[? <: Option[?]], path: Path, paramStruct: Structure) extends Structure + object Optional { + def fromWrapped(wrapped: Wrapped[Option]): Optional = + Optional(wrapped.tpe, wrapped.path, wrapped.underlying) + } + case class Collection(tpe: Type[? <: Iterable[?]], path: Path, paramStruct: Structure) extends Structure case class Singleton(tpe: Type[?], path: Path, name: String, value: Expr[Any]) extends Structure @@ -60,7 +65,7 @@ private[ducktape] object Structure { case class ValueClass(tpe: Type[? <: AnyVal], path: Path, paramTpe: Type[?], paramFieldName: String) extends Structure - case class Wrappped[F[+x]](tpe: Type[? <: F[Any]], path: Path, underlying: Structure) extends Structure + case class Wrapped[F[+x]](tpe: Type[? <: F[Any]], wrapper: WrapperType[F], path: Path, underlying: Structure) extends Structure case class Lazy private (tpe: Type[?], path: Path, private val deferredStruct: () => Structure) extends Structure { lazy val struct: Structure = deferredStruct() @@ -95,20 +100,21 @@ private[ducktape] object Structure { case tpe @ '[Nothing] => Structure.Ordinary(tpe, path) + case WrapperType(wrapper: WrapperType[f], '[underlying]) => + @unused given Type[f] = wrapper.wrapper + Structure.Wrapped( + Type.of[f[underlying]], + wrapper, + path, + Structure.of[underlying](path.appended(Path.Segment.Element(Type.of[underlying]))) + ) + case tpe @ '[Option[param]] => Structure.Optional(tpe, path, Structure.of[param](path.appended(Path.Segment.Element(Type.of[param])))) case tpe @ '[Iterable[param]] => Structure.Collection(tpe, path, Structure.of[param](path.appended(Path.Segment.Element(Type.of[param])))) - case WrapperType(wrapper: WrapperType.Wrapped[f], '[underlying]) => - @unused given Type[f] = wrapper.wrapperTpe - Structure.Wrappped( - Type.of[f[underlying]], - path, - Structure.of[underlying](path.appended(Path.Segment.Element(Type.of[underlying]))) - ) - case tpe @ '[AnyVal] if tpe.repr.typeSymbol.flags.is(Flags.Case) => val repr = tpe.repr val param = repr.typeSymbol.caseFields.head @@ -216,10 +222,12 @@ private[ducktape] object Structure { curr match { case '[head *: tail] => loop(Type.of[tail], TypeRepr.of[head] :: acc) - case '[EmptyTuple] => + case '[EmptyTuple] => acc case other => - report.errorAndAbort(s"Unexpected type (${other.repr.show}) encountered when extracting tuple type elems. This is a bug in ducktape.") + report.errorAndAbort( + s"Unexpected type (${other.repr.show}) encountered when extracting tuple type elems. This is a bug in ducktape." + ) } } diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/WrapperType.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/WrapperType.scala index c88f4603..28fc1868 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/WrapperType.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/WrapperType.scala @@ -1,13 +1,35 @@ package io.github.arainko.ducktape.internal +import io.github.arainko.ducktape.internal.Debug.AST + import scala.annotation.unused import scala.quoted.* -private[ducktape] sealed trait WrapperType { - def unapply(tpe: Type[?])(using Quotes): Option[(WrapperType, Type[?])] +private[ducktape] sealed trait WrapperType[F[+x]] { + def wrapper(using Quotes): Type[F] + + def unapply(tpe: Type[?])(using Quotes): Option[(WrapperType[F], Type[?])] } private[ducktape] object WrapperType { + def create[F[+x]: Type](using Quotes): WrapperType[F] = { + import quotes.reflect.* + + Type.of[F[Any]] match { + case '[Option[a]] => + Optional.asInstanceOf[WrapperType[F]] + case other => + Wrapped(Type.of[F]) + } + } + + given Debug[WrapperType[?]] with { + def astify(self: WrapperType[?])(using Quotes): AST = + import quotes.reflect.* + self match + case Optional => Debug.AST.Text(s"WrapperType[Option]") + case Wrapped(wrapperTpe) => Debug.AST.Text(s"WrapperType[${wrapperTpe.repr.show(using Printer.TypeReprShortCode)}]") + } def unapply(using Quotes, Context)(tpe: Type[?]) = Context.current match { @@ -15,12 +37,22 @@ private[ducktape] object WrapperType { case Context.Total(_) => None } - case object Absent extends WrapperType { - override def unapply(tpe: Type[? <: AnyKind])(using Quotes): Option[(WrapperType, Type[?])] = None + case object Optional extends WrapperType[Option] { + + def wrapper(using Quotes): Type[Option] = Type.of[Option] + + override def unapply(tpe: Type[? <: AnyKind])(using Quotes): Option[(WrapperType[Option], Type[?])] = { + tpe match { + case '[Option[underlying]] => Some(this -> Type.of[underlying]) + case _ => None + } + } } - final case class Wrapped[F[+x]](wrapperTpe: Type[F]) extends WrapperType { - override def unapply(tpe: Type[? <: AnyKind])(using Quotes): Option[(WrapperType, Type[?])] = { + final case class Wrapped[F[+x]] private[WrapperType] (wrapperTpe: Type[F]) extends WrapperType[F] { + def wrapper(using Quotes): Type[F] = wrapperTpe + + override def unapply(tpe: Type[? <: AnyKind])(using Quotes): Option[(WrapperType[F], Type[?])] = { @unused given Type[F] = wrapperTpe tpe match case '[F[underlying]] => Some(this -> Type.of[underlying]) diff --git a/ducktape/src/test/scala/io/github/arainko/ducktape/issues/Issue187Suite.scala b/ducktape/src/test/scala/io/github/arainko/ducktape/issues/Issue187Suite.scala index ed9c1c64..be9e1fbe 100644 --- a/ducktape/src/test/scala/io/github/arainko/ducktape/issues/Issue187Suite.scala +++ b/ducktape/src/test/scala/io/github/arainko/ducktape/issues/Issue187Suite.scala @@ -2,15 +2,93 @@ package io.github.arainko.ducktape.issues import io.github.arainko.ducktape.* class Issue187Suite extends DucktapeSuite { - test("minimization works") { - + test("BetweenNonOptionOption works when Mode[Option] is in scope") { + given Transformer.Fallible[Option, Int, String] = a => Some(a.toString) - case class Source(int: Int) + case class Source(int: Int, str: String) + case class Dest(int: Option[String], str: Option[String]) + + Mode.FailFast.option.locally { + val source = Source(1, "str") + val expected = Dest(Some("1"), Some("str")) + + assertTransformsFallible( + source, + Some(expected) + ) + assertEachEquals( + source.fallibleVia(Dest.apply), + Transformer.defineVia[Source](Dest.apply).fallible.build().transform(source) + )(Some(expected)) + } + } + + test("BetweenOptions works when Mode[Option] is in scope") { + given Transformer.Fallible[Option, Int, String] = a => Some(a.toString) + + case class Source(int: Option[Int]) case class Dest(int: Option[String]) Mode.FailFast.option.locally { - Source(1).fallibleTo[Dest] + val source = Source(Some(1)) + val expected = Dest(Some("1")) + + assertTransformsFallible( + source, + Some(expected) + ) + assertEachEquals( + source.fallibleVia(Dest.apply), + Transformer.defineVia[Source](Dest.apply).fallible.build().transform(source) + )(Some(expected)) + + } + } + + test("Fallible transformation for an Option works when Mode[Option] is in scope") { + given Transformer.Fallible[Option, Int, String] = a => Some(a.toString) + + case class Source(int: Option[Int]) + case class Dest(int: String) + + Mode.FailFast.option.locally { + val source = Source(Some(1)) + val expected = Dest("1") + + assertTransformsFallible( + source, + Some(expected) + ) + assertEachEquals( + source.fallibleVia(Dest.apply), + Transformer.defineVia[Source](Dest.apply).fallible.build().transform(source) + )(Some(expected)) + } + } + + test("Option-unwrapping works") { + case class Dest(int1: Int, int2: Int, int3: Int, int4: Int) + + Mode.FailFast.option.locally { + val source = + ( + Some(1), + Some(2), + Some(3), + Some(4) + ) + + val expected = Dest(1, 2, 3, 4) + + assertTransformsFallible( + source, + Some(expected) + ) + assertEachEquals( + source.fallibleVia(Dest.apply), + Transformer.defineVia[source.type](Dest.apply).fallible.build().transform(source) + )(Some(expected)) } } } diff --git a/ducktape/src/test/scala/io/github/arainko/ducktape/issues/Issue190Suite.scala b/ducktape/src/test/scala/io/github/arainko/ducktape/issues/Issue190Suite.scala index 71f1c492..a967b971 100644 --- a/ducktape/src/test/scala/io/github/arainko/ducktape/issues/Issue190Suite.scala +++ b/ducktape/src/test/scala/io/github/arainko/ducktape/issues/Issue190Suite.scala @@ -3,17 +3,23 @@ package io.github.arainko.ducktape.issues import io.github.arainko.ducktape.* class Issue190Suite extends DucktapeSuite { - - test("transforming from tuples concatenated together works") { + + test("transforming from concatenated together tuples works (bound to an intermediate val)") { case class Big(int1: Int, int2: Int, int3: Int, int4: Int, int5: Int, int6: Int, int7: Int, int8: Int) val one = (1, 2, 3, 4) val two = (5, 6, 7, 8) - val joined = (one ++ two) + val joined = one ++ two val expected = Big(1, 2, 3, 4, 5, 6, 7, 8) + assertTransforms(joined, expected) + assertEachEquals( + joined.via(Big.apply), + joined.intoVia(Big.apply).transform(), + Transformer.defineVia[joined.type](Big.apply).build().transform(joined) + )(expected) } }