From 91047c50e8a3bf25011831f18b0cfc01ce1c039e Mon Sep 17 00:00:00 2001 From: Aleksander Rainko Date: Wed, 11 Sep 2024 22:03:17 +0200 Subject: [PATCH 01/11] initial work, come up with a sensible DSL for computedDeep --- .sbtopts | 3 ++ build.sbt | 4 +-- .../io/github/arainko/ducktape/Field.scala | 33 +++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 .sbtopts diff --git a/.sbtopts b/.sbtopts new file mode 100644 index 00000000..cf6af24a --- /dev/null +++ b/.sbtopts @@ -0,0 +1,3 @@ +-J-Xmx5G +-J-Xms1G +-J-Xss2M diff --git a/build.sbt b/build.sbt index 54872347..742189a9 100644 --- a/build.sbt +++ b/build.sbt @@ -43,7 +43,7 @@ ThisBuild / tlVersionIntroduced := Map("3" -> "0.1.6") lazy val root = tlCrossRootProject.aggregate(ducktape) lazy val ducktape = - crossProject(JVMPlatform, JSPlatform, NativePlatform) + crossProject(JVMPlatform/*, JSPlatform, NativePlatform*/) .crossType(CrossType.Pure) .enablePlugins(TypelevelMimaPlugin) .in(file("ducktape")) @@ -53,7 +53,7 @@ lazy val ducktape = Test / scalacOptions ++= List("-Werror", "-Wconf:cat=deprecation:s"), libraryDependencies += "org.scalameta" %%% "munit" % "1.0.1" % Test ) - .nativeSettings(tlMimaPreviousVersions := Set.empty) + //.nativeSettings(tlMimaPreviousVersions := Set.empty) lazy val docs = project diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/Field.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/Field.scala index c45d3052..0a977333 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/Field.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/Field.scala @@ -22,6 +22,11 @@ object Field { function: A => F[DestFieldTpe] ): Field.Fallible[F, A, B] = ??? + def fallibleComputedDeep[F[+x], A, B, DestFieldTpe, SourceFieldTpe, ComputedTpe]( + selector: Selector ?=> B => DestFieldTpe, + function: SourceFieldTpe => ComputedTpe + ): Field.Fallible[F, A, B] = ??? + @compileTimeOnly("Field.const is only useable as a field configuration for transformations") def const[A, B, DestFieldTpe, ConstTpe](selector: Selector ?=> B => DestFieldTpe, value: ConstTpe): Field[A, B] = ??? @@ -31,6 +36,11 @@ object Field { function: A => ComputedTpe ): Field[A, B] = ??? + def computedDeep[A, B, DestFieldTpe, SourceFieldTpe, ComputedTpe]( + selector: Selector ?=> B => DestFieldTpe, + function: SourceFieldTpe => ComputedTpe + ): Field[A, B] = ??? + @compileTimeOnly("Field.renamed is only useable as a field configuration for transformations") def renamed[A, B, DestFieldTpe, SourceFieldTpe]( destSelector: Selector ?=> B => DestFieldTpe, @@ -53,4 +63,27 @@ object Field { @compileTimeOnly("Field.allMatching is only useable as a field configuration for transformations") def allMatching[A, B, ProductTpe](product: ProductTpe): Field[A, B] = ??? + + inline def of[A]: Field.Of[A] = () + + opaque type Of[A] = Unit + + extension [A] (inline self: Of[A]) { + inline def apply[B](f: A => B): A => B = f + } +} + +object test { + // import other.* + + def dupal[A, B](f: A => B) = f + + val b = dupal(method) + + Transformer.Debug.showCode: + val e = dupal(Field.of[Int](_.toString)) + + def method(str: Int): String = ??? + + } From cd58aebf97f83bdbd6bce93d07426e2869b9d7e9 Mon Sep 17 00:00:00 2001 From: Aleksander Rainko Date: Sat, 28 Sep 2024 21:35:29 +0200 Subject: [PATCH 02/11] fix warnings --- .../github/arainko/ducktape/Transformer.scala | 1 - .../arainko/ducktape/deprecatedAliases.scala | 1 - .../arainko/ducktape/internal/Debug.scala | 20 +++++++++++-------- .../ducktape/internal/NonEmptyList.scala | 3 +++ .../ducktape/internal/PathSelector.scala | 2 -- .../arainko/ducktape/internal/Plan.scala | 2 -- .../internal/TotalTransformations.scala | 1 - 7 files changed, 15 insertions(+), 15 deletions(-) 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 1a153e78..0d93908c 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/Transformer.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/Transformer.scala @@ -1,7 +1,6 @@ package io.github.arainko.ducktape import io.github.arainko.ducktape -import io.github.arainko.ducktape.DefinitionViaBuilder.PartiallyApplied import io.github.arainko.ducktape.Transformer.Derived.FromFunction import io.github.arainko.ducktape.internal.{ FallibleTransformations, TotalTransformations } diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/deprecatedAliases.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/deprecatedAliases.scala index d2b039bd..c8f29f06 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/deprecatedAliases.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/deprecatedAliases.scala @@ -1,7 +1,6 @@ package io.github.arainko.ducktape import io.github.arainko.ducktape -import io.github.arainko.ducktape.* import io.github.arainko.ducktape.Transformer.Fallible package fallible { diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Debug.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Debug.scala index 02b41aa8..97f35bd2 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Debug.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Debug.scala @@ -101,11 +101,9 @@ private[ducktape] object Debug extends LowPriorityDebug { case given Mirror.SumOf[A] => coproduct } - private inline def product[A](using A: Mirror.ProductOf[A]): Debug[A] = - new { - def astify(self: A)(using Quotes): AST = { - val tpeName = constValue[A.MirroredLabel].toString - val instances = summonAll[Tuple.Map[A.MirroredElemTypes, Debug]].toIArray.map(_.asInstanceOf[Debug[Any]]) + private[ducktape] class ForProduct[A](tpeName: String, _instances: => IArray[Debug[Any]]) extends Debug[A] { + private lazy val instances = _instances + def astify(self: A)(using Quotes): AST = { val prod = self.asInstanceOf[scala.Product] val fields = prod.productElementNames .zip(instances) @@ -118,17 +116,23 @@ private[ducktape] object Debug extends LowPriorityDebug { Product(tpeName, fields) } - } + } - private inline def coproduct[A](using A: Mirror.SumOf[A]): Debug[A] = new { - private val instances = deriveForAll[A.MirroredElemTypes].toVector + private inline def product[A](using A: Mirror.ProductOf[A]): Debug[A] = { + val tpeName = constValue[A.MirroredLabel].toString + def instances = summonAll[Tuple.Map[A.MirroredElemTypes, Debug]].toIArray.map(_.asInstanceOf[Debug[Any]]) + ForProduct(tpeName, instances) + } + private[ducktape] class ForCoproduct[A](instances: Vector[Debug[Any]])(using A: Mirror.SumOf[A]) extends Debug[A] { def astify(self: A)(using Quotes): AST = { val ordinal = A.ordinal(self) instances(ordinal).astify(self) } } + private inline def coproduct[A](using A: Mirror.SumOf[A]): Debug[A] = ForCoproduct(deriveForAll[A.MirroredElemTypes].toVector) + private inline def deriveForAll[Tup <: Tuple]: List[Debug[Any]] = inline erasedValue[Tup] match { case _: (head *: tail) => diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/NonEmptyList.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/NonEmptyList.scala index bdf587e3..89d5812d 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/NonEmptyList.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/NonEmptyList.scala @@ -1,5 +1,7 @@ package io.github.arainko.ducktape.internal +import scala.annotation.nowarn + private[ducktape] opaque type NonEmptyList[+A] = ::[A] private[ducktape] object NonEmptyList { @@ -25,6 +27,7 @@ private[ducktape] object NonEmptyList { private[ducktape] def ::(elem: A): NonEmptyList[A] = Cons(elem, self) + @nowarn private[ducktape] def :::(that: List[A]): NonEmptyList[A] = unsafeCoerce(toList ::: that) private[ducktape] def map[B](f: A => B): NonEmptyList[B] = unsafeCoerce(toList.map(f)) diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PathSelector.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PathSelector.scala index e8dd499a..ad67341c 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PathSelector.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PathSelector.scala @@ -5,8 +5,6 @@ import scala.quoted.* private[ducktape] object PathSelector { def unapply(using Quotes)(expr: quotes.reflect.Term): Some[Path] = { - import quotes.reflect.{ Selector as _, * } - @tailrec def recurse(using Quotes)(acc: List[Path.Segment], term: quotes.reflect.Term): Path = { import quotes.reflect.* 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 ae1b6875..061ffc3c 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 @@ -15,8 +15,6 @@ private[ducktape] object Erroneous private[ducktape] type Erroneous = Erroneous.type private[ducktape] sealed trait Plan[+E <: Erroneous, +F <: Fallible] { - import Plan.* - def source: Structure def dest: Structure diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/TotalTransformations.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/TotalTransformations.scala index 1a9aaa95..ac256406 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/TotalTransformations.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/TotalTransformations.scala @@ -1,7 +1,6 @@ package io.github.arainko.ducktape.internal import io.github.arainko.ducktape.* -import io.github.arainko.ducktape.internal.Function.fromFunctionArguments import scala.quoted.* From 41c8b726aad244b9dd79d9849ff8b37a8996a93d Mon Sep 17 00:00:00 2001 From: Aleksander Rainko Date: Sat, 28 Sep 2024 23:40:27 +0200 Subject: [PATCH 03/11] implement a POC --- .../io/github/arainko/ducktape/Field.scala | 4 +-- .../io/github/arainko/ducktape/Mode.scala | 14 ++++++++ .../internal/ConfigInstructionRefiner.scala | 4 +-- .../ducktape/internal/ConfigParser.scala | 33 +++++++++++++++++++ .../ducktape/internal/Configuration.scala | 2 ++ .../internal/FallibilityRefiner.scala | 16 +++++---- .../internal/FalliblePlanInterpreter.scala | 8 +++++ .../arainko/ducktape/internal/Logger.scala | 2 +- .../ducktape/internal/PlanConfigurer.scala | 24 +++++++++++++- .../ducktape/internal/PlanInterpreter.scala | 6 ++++ .../internal/TotalTransformations.scala | 4 ++- 11 files changed, 103 insertions(+), 14 deletions(-) diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/Field.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/Field.scala index 0a977333..3f89b8ce 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/Field.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/Field.scala @@ -22,9 +22,9 @@ object Field { function: A => F[DestFieldTpe] ): Field.Fallible[F, A, B] = ??? - def fallibleComputedDeep[F[+x], A, B, DestFieldTpe, SourceFieldTpe, ComputedTpe]( + def fallibleComputedDeep[F[+x], A, B, DestFieldTpe, SourceFieldTpe]( selector: Selector ?=> B => DestFieldTpe, - function: SourceFieldTpe => ComputedTpe + function: SourceFieldTpe => F[DestFieldTpe] ): Field.Fallible[F, A, B] = ??? @compileTimeOnly("Field.const is only useable as a field configuration for transformations") diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/Mode.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/Mode.scala index 7b46fd6e..baba752c 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/Mode.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/Mode.scala @@ -157,3 +157,17 @@ object Mode { def either[E]: Mode.FailFast.Either[E] = Mode.FailFast.Either[E] } } + + +object test2 { + import io.github.arainko.ducktape.* + + case class Source(opt: Option[Level1]) + case class Level1(int: Int) + + // internal.CodePrinter.code: + Source(Some(Level1(2))) + .into[Source] + .transform(Field.computedDeep(_.opt.element.int, (int: Int) => int + 1)) + +} 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 9c4ac60e..5c41153f 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 @@ -9,8 +9,8 @@ private[ducktape] object ConfigInstructionRefiner { instruction match case inst @ Instruction.Static(_, _, config, _) => config match - case cfg: (Const | CaseComputed | FieldComputed | FieldReplacement) => inst.copy(config = cfg) - case fallible: (FallibleConst | FallibleFieldComputed | FallibleCaseComputed) => None + case cfg: (Const | CaseComputed | FieldComputed | FieldComputedDeep | FieldReplacement) => inst.copy(config = cfg) + case fallible: (FallibleConst | FallibleFieldComputed | FallibleFieldComputedDeep | FallibleCaseComputed) => None case inst: (Instruction.Dynamic | Instruction.Bulk | Instruction.Regional | Instruction.Failed) => inst } diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/ConfigParser.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/ConfigParser.scala index 60c3a06a..664864d5 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/ConfigParser.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/ConfigParser.scala @@ -74,6 +74,24 @@ private[ducktape] object ConfigParser { Span.fromPosition(cfg.pos) ) + case cfg @ Apply( + TypeApply( + Select(IdentOfType('[Field.type]), "computedDeep"), + a :: b :: destFieldTpe :: sourceFieldTpe :: computedFieldTpe :: Nil + ), + PathSelector(path) :: function :: Nil + ) => + Configuration.Instruction.Static( + path, + Side.Dest, + Configuration.FieldComputedDeep( + computedFieldTpe.tpe.asType, + sourceFieldTpe.tpe.asType, + function.asExpr.asInstanceOf[Expr[Any => Any]] + ), + Span.fromPosition(cfg.pos) + ) + case cfg @ Apply( TypeApply(Select(IdentOfType('[Field.type]), "allMatching"), a :: b :: destFieldTpe :: fieldSourceTpe :: Nil), PathSelector(path) :: fieldSource :: Nil @@ -173,6 +191,21 @@ private[ducktape] object ConfigParser { Span.fromPosition(cfg.pos) ) + case cfg @ Apply( + TypeApply( + Select(IdentOfType('[Field.type]), "fallibleComputedDeep"), + f :: a :: b :: destFieldTpe :: sourceFieldTpe :: Nil + ), + PathSelector(path) :: AsExpr('{ $function: (a => F[computed]) }) :: Nil + ) => + Configuration.Instruction.Static( + path, + Side.Dest, + Configuration + .FallibleFieldComputedDeep(Type.of[computed], sourceFieldTpe.tpe.asType, function.asInstanceOf[Expr[Any => Any]]), + Span.fromPosition(cfg.pos) + ) + case cfg @ Apply( TypeApply(Select(IdentOfType('[Case.type]), "fallibleConst"), f :: a :: b :: sourceTpe :: constTpe :: Nil), PathSelector(path) :: AsExpr('{ $value: F[const] }) :: Nil diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Configuration.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Configuration.scala index 5a185053..f49c53a8 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Configuration.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Configuration.scala @@ -10,9 +10,11 @@ private[ducktape] enum Configuration[+F <: Fallible] { case Const(value: Expr[Any], tpe: Type[?]) extends Configuration[Nothing] case CaseComputed(tpe: Type[?], function: Expr[Any => Any]) extends Configuration[Nothing] case FieldComputed(tpe: Type[?], function: Expr[Any => Any]) extends Configuration[Nothing] + case FieldComputedDeep(tpe: Type[?], sourceTpe: Type[?], function: Expr[Any => Any]) extends Configuration[Nothing] case FieldReplacement(source: Expr[Any], name: String, tpe: Type[?]) extends Configuration[Nothing] case FallibleConst(value: Expr[Any], tpe: Type[?]) extends Configuration[Fallible] case FallibleFieldComputed(tpe: Type[?], function: Expr[Any => Any]) extends Configuration[Fallible] + case FallibleFieldComputedDeep(tpe: Type[?], sourceTpe: Type[?], function: Expr[Any => Any]) extends Configuration[Fallible] case FallibleCaseComputed(tpe: Type[?], function: Expr[Any => Any]) extends Configuration[Fallible] } diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/FallibilityRefiner.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/FallibilityRefiner.scala index 5d828557..5e2a4b25 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/FallibilityRefiner.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/FallibilityRefiner.scala @@ -30,13 +30,15 @@ private[ducktape] object FallibilityRefiner { case Configured(source, dest, config, _) => config match - case Configuration.Const(value, tpe) => () - case Configuration.CaseComputed(tpe, function) => () - case Configuration.FieldComputed(tpe, function) => () - case Configuration.FieldReplacement(source, name, tpe) => () - case Configuration.FallibleConst(value, tpe) => boundary.break(None) - case Configuration.FallibleFieldComputed(tpe, function) => boundary.break(None) - case Configuration.FallibleCaseComputed(tpe, function) => boundary.break(None) + case Configuration.Const(value, tpe) => () + case Configuration.CaseComputed(tpe, function) => () + case Configuration.FieldComputed(tpe, function) => () + case Configuration.FieldComputedDeep(tpe, srcTpe, function) => () + case Configuration.FieldReplacement(source, name, tpe) => () + case Configuration.FallibleConst(value, tpe) => boundary.break(None) + case Configuration.FallibleFieldComputed(tpe, function) => boundary.break(None) + case Configuration.FallibleFieldComputedDeep(tpe, srcTpe, function) => boundary.break(None) + case Configuration.FallibleCaseComputed(tpe, function) => boundary.break(None) case BetweenProductFunction(source, dest, argPlans) => evaluate(argPlans.values) diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/FalliblePlanInterpreter.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/FalliblePlanInterpreter.scala index 0692aa1e..ecd20bb7 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/FalliblePlanInterpreter.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/FalliblePlanInterpreter.scala @@ -38,6 +38,8 @@ private[ducktape] object FalliblePlanInterpreter { Value.Unwrapped(PlanInterpreter.evaluateConfig(cfg, value)) case cfg @ Configuration.FieldComputed(tpe, function) => Value.Unwrapped(PlanInterpreter.evaluateConfig(cfg, value)) + case cfg @ Configuration.FieldComputedDeep(tpe, srcTpe, function) => + Value.Unwrapped(PlanInterpreter.evaluateConfig(cfg, value)) case cfg @ Configuration.FieldReplacement(source, name, tpe) => Value.Unwrapped(PlanInterpreter.evaluateConfig(cfg, value)) case Configuration.FallibleConst(value, tpe) => @@ -51,6 +53,12 @@ private[ducktape] object FalliblePlanInterpreter { Value.Wrapped('{ $function($toplevelValue) }.asExprOf[F[tpe]]) } + case Configuration.FallibleFieldComputedDeep(tpe, srcTpe, function) => + tpe match { + case '[tpe] => + Value.Wrapped('{ $function($value) }.asExprOf[F[tpe]]) + } + case Configuration.FallibleCaseComputed(tpe, function) => tpe match { case '[tpe] => 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 afd1b581..fd869f7b 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.Info 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/PlanConfigurer.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanConfigurer.scala index 6e58f03b..a2501752 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanConfigurer.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanConfigurer.scala @@ -426,7 +426,29 @@ private[ducktape] object PlanConfigurer { context: Context ) = { def isReplaceableBy(update: Configuration[F])(using Quotes) = - update.tpe.repr <:< currentPlan.destPath.currentTpe.repr + def checkDestTpe = update.tpe.repr <:< currentPlan.destPath.currentTpe.repr + def checkSourceTpe(srcTpe: Type[?]) = currentPlan.sourcePath.currentTpe.repr <:< srcTpe.repr + + //TODO: Make this nicer, this should also report which sourceType was actually expected as opposed to what was provided + update match + case Configuration.Const(value, tpe) => + checkDestTpe + case Configuration.CaseComputed(tpe, function) => + checkDestTpe + case Configuration.FieldComputed(tpe, function) => + checkDestTpe + case Configuration.FieldComputedDeep(tpe, sourceTpe, function) => + checkDestTpe && checkSourceTpe(sourceTpe) + case Configuration.FieldReplacement(source, name, tpe) => + checkDestTpe + case Configuration.FallibleConst(value, tpe) => + checkDestTpe + case Configuration.FallibleFieldComputed(tpe, function) => + checkDestTpe + case Configuration.FallibleFieldComputedDeep(tpe, sourceTpe, function) => + checkDestTpe && checkSourceTpe(sourceTpe) + case Configuration.FallibleCaseComputed(tpe, function) => + checkDestTpe if isReplaceableBy(config) then val (path, _) = diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanInterpreter.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanInterpreter.scala index 1ded8e5f..56652a21 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanInterpreter.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanInterpreter.scala @@ -24,8 +24,11 @@ private[ducktape] object PlanInterpreter { case Plan.BetweenProducts(sourceTpe, destTpe, fieldPlans) => val args = fieldPlans.map { + case (fieldName, p: Plan.Configured[Nothing]) if sourceTpe.fields.contains(fieldName) => + NamedArg(fieldName, recurse(p, value.accessFieldByName(fieldName).asExpr).asTerm) case (fieldName, p: Plan.Configured[Nothing]) => NamedArg(fieldName, recurse(p, value).asTerm) + case (fieldName, plan) => val fieldValue = value.accessFieldByName(fieldName).asExpr NamedArg(fieldName, recurse(plan, fieldValue).asTerm) @@ -35,6 +38,7 @@ private[ducktape] object PlanInterpreter { case Plan.BetweenProductTuple(source, dest, plans) => val fields = source.fields.keys val args = plans.zipWithIndex.map { + //TODO: All other Product/Tuple or vice versa things need to handle passing in the value if they can case (p: Plan.Configured[Nothing], _) => recurse(p, value) case (plan, index) => @@ -159,6 +163,8 @@ private[ducktape] object PlanInterpreter { '{ $function.apply($value) } case Configuration.FieldComputed(_, function) => '{ $function.apply($toplevelValue) } + case Configuration.FieldComputedDeep(tpe, sourceTpe, function) => + '{ $function.apply($value) } case Configuration.FieldReplacement(source, name, tpe) => source.accessFieldByName(name).asExpr } diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/TotalTransformations.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/TotalTransformations.scala index ac256406..2230dbf7 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/TotalTransformations.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/TotalTransformations.scala @@ -23,7 +23,9 @@ private[ducktape] object TotalTransformations { val plan = Planner.between(Structure.of[A](Path.empty(Type.of[A])), Structure.of[B](Path.empty(Type.of[B]))) val config = Configuration.parse(configs, ConfigParser.total) val totalPlan = Backend.refineOrReportErrorsAndAbort(plan, config) - PlanInterpreter.run[A](totalPlan, value).asExprOf[B] + val res = PlanInterpreter.run[A](totalPlan, value).asExprOf[B] + Logger.info(res.show) + res } inline def via[A, B, Func, Args <: FunctionArguments]( From 64f295cc424b6ab123267c58d9054cedd11a2796 Mon Sep 17 00:00:00 2001 From: Aleksander Rainko Date: Sat, 5 Oct 2024 22:59:26 +0200 Subject: [PATCH 04/11] properly pass values in interpreters, report wrong source error --- .../io/github/arainko/ducktape/Field.scala | 25 +---- .../io/github/arainko/ducktape/Mode.scala | 17 ++-- .../ducktape/internal/Configuration.scala | 33 ++++--- .../ducktape/internal/ErrorMessage.scala | 11 ++- .../internal/FalliblePlanInterpreter.scala | 18 ++-- .../arainko/ducktape/internal/Logger.scala | 2 +- .../arainko/ducktape/internal/Plan.scala | 2 +- .../ducktape/internal/PlanConfigurer.scala | 98 ++++++++++--------- .../ducktape/internal/PlanInterpreter.scala | 50 +++++----- 9 files changed, 133 insertions(+), 123 deletions(-) diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/Field.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/Field.scala index 3f89b8ce..059cb30b 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/Field.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/Field.scala @@ -64,26 +64,11 @@ object Field { def allMatching[A, B, ProductTpe](product: ProductTpe): Field[A, B] = ??? - inline def of[A]: Field.Of[A] = () + // inline def of[A]: Field.Of[A] = () - opaque type Of[A] = Unit + // opaque type Of[A] = Unit - extension [A] (inline self: Of[A]) { - inline def apply[B](f: A => B): A => B = f - } -} - -object test { - // import other.* - - def dupal[A, B](f: A => B) = f - - val b = dupal(method) - - Transformer.Debug.showCode: - val e = dupal(Field.of[Int](_.toString)) - - def method(str: Int): String = ??? - - + // extension [A] (inline self: Of[A]) { + // inline def apply[B](f: A => B): A => B = f + // } } diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/Mode.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/Mode.scala index baba752c..e88360a8 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/Mode.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/Mode.scala @@ -158,16 +158,17 @@ object Mode { } } - object test2 { - import io.github.arainko.ducktape.* + // import io.github.arainko.ducktape.* - case class Source(opt: Option[Level1]) - case class Level1(int: Int) + private case class Source(opt: Option[Level1]) + private case class Level1(int: Int) - // internal.CodePrinter.code: - Source(Some(Level1(2))) - .into[Source] - .transform(Field.computedDeep(_.opt.element.int, (int: Int) => int + 1)) + internal.CodePrinter.code: + locally { + Source(Some(Level1(2))) + .into[Source] + .transform(Field.computedDeep(_.opt.element.int, (int: Any) => int.toString.toInt + 1)) + } } diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Configuration.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Configuration.scala index f49c53a8..8b52543c 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Configuration.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Configuration.scala @@ -5,17 +5,28 @@ import io.github.arainko.ducktape.* import scala.quoted.* private[ducktape] enum Configuration[+F <: Fallible] { - def tpe: Type[?] - - case Const(value: Expr[Any], tpe: Type[?]) extends Configuration[Nothing] - case CaseComputed(tpe: Type[?], function: Expr[Any => Any]) extends Configuration[Nothing] - case FieldComputed(tpe: Type[?], function: Expr[Any => Any]) extends Configuration[Nothing] - case FieldComputedDeep(tpe: Type[?], sourceTpe: Type[?], function: Expr[Any => Any]) extends Configuration[Nothing] - case FieldReplacement(source: Expr[Any], name: String, tpe: Type[?]) extends Configuration[Nothing] - case FallibleConst(value: Expr[Any], tpe: Type[?]) extends Configuration[Fallible] - case FallibleFieldComputed(tpe: Type[?], function: Expr[Any => Any]) extends Configuration[Fallible] - case FallibleFieldComputedDeep(tpe: Type[?], sourceTpe: Type[?], function: Expr[Any => Any]) extends Configuration[Fallible] - case FallibleCaseComputed(tpe: Type[?], function: Expr[Any => Any]) extends Configuration[Fallible] + def destTpe: Type[?] + def sourceTpe: Type[?] | None.type = None + + case Const(value: Expr[Any], destTpe: Type[?]) extends Configuration[Nothing] + + case CaseComputed(destTpe: Type[?], function: Expr[Any => Any]) extends Configuration[Nothing] + + case FieldComputed(destTpe: Type[?], function: Expr[Any => Any]) extends Configuration[Nothing] + + case FieldComputedDeep(destTpe: Type[?], override val sourceTpe: Type[?], function: Expr[Any => Any]) + extends Configuration[Nothing] + + case FieldReplacement(source: Expr[Any], name: String, destTpe: Type[?]) extends Configuration[Nothing] + + case FallibleConst(value: Expr[Any], destTpe: Type[?]) extends Configuration[Fallible] + + case FallibleFieldComputed(destTpe: Type[?], function: Expr[Any => Any]) extends Configuration[Fallible] + + case FallibleFieldComputedDeep(destTpe: Type[?], override val sourceTpe: Type[?], function: Expr[Any => Any]) + extends Configuration[Fallible] + + case FallibleCaseComputed(destTpe: Type[?], function: Expr[Any => Any]) extends Configuration[Fallible] } private[ducktape] object Configuration { diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/ErrorMessage.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/ErrorMessage.scala index 242ac682..c552bda1 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/ErrorMessage.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/ErrorMessage.scala @@ -43,7 +43,7 @@ private[ducktape] object ErrorMessage { val side = Side.Source } - final case class InvalidConfiguration(configTpe: Type[?], expectedTpe: Type[?], side: Side, span: Span) extends ErrorMessage { + final case class InvalidConfigurationDestType(configTpe: Type[?], expectedTpe: Type[?], side: Side, span: Span) extends ErrorMessage { def render(using Quotes): String = { val renderedConfigTpe = configTpe.repr.show @@ -52,6 +52,15 @@ private[ducktape] object ErrorMessage { } } + final case class InvalidConfigurationSourceType(configTpe: Type[?], expectedTpe: Type[?], side: Side, span: Span) extends ErrorMessage { + + def render(using Quotes): String = { + val renderedConfigTpe = configTpe.repr.show + val renderedExpectedTpe = expectedTpe.repr.show + s"Configuration is not valid since the provided source type (${renderedConfigTpe}) is not a supertype of ${renderedExpectedTpe}" + } + } + final case class CouldntBuildTransformation(source: Type[?], dest: Type[?]) extends ErrorMessage { def render(using Quotes): String = s"Couldn't build a transformation plan between ${source.repr.show} and ${dest.repr.show}" def span = None diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/FalliblePlanInterpreter.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/FalliblePlanInterpreter.scala index ecd20bb7..f7413fb3 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/FalliblePlanInterpreter.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/FalliblePlanInterpreter.scala @@ -240,11 +240,11 @@ private[ducktape] object FalliblePlanInterpreter { val (unwrapped, wrapped) = plans.zipWithIndex.partitionMap { - case (p: Plan.Configured[Fallible]) -> index => - recurse(p, value, F).asFieldValue(index, p.dest.tpe) - case plan -> index => + case plan -> index if sourceStruct.elements.isDefinedAt(index) => val fieldValue = value.accesFieldByIndex(index, sourceStruct) recurse(plan, fieldValue, F).asFieldValue(index, plan.dest.tpe) + case plan -> index => + recurse(plan, value, F).asFieldValue(index, plan.dest.tpe) } plan.dest.tpe match { @@ -283,22 +283,22 @@ private[ducktape] object FalliblePlanInterpreter { def handleVectorMap(fieldPlans: VectorMap[String, Plan[Nothing, Fallible]])(using Quotes) = fieldPlans.zipWithIndex.partitionMap { - case (fieldName, p: Plan.Configured[Fallible]) -> index => - recurse(p, value, F).asFieldValue(index, p.dest.tpe) - case (fieldName, plan) -> index => + case (fieldName, plan) -> index if source.fields.contains(fieldName) => val fieldValue = value.accessFieldByName(fieldName).asExpr recurse(plan, fieldValue, F).asFieldValue(index, plan.dest.tpe) + case (fieldName, plan) -> index => + recurse(plan, value, F).asFieldValue(index, plan.dest.tpe) } def handleVector(fieldPlans: Vector[Plan[Nothing, Fallible]])(using Quotes) = { val sourceFields = source.fields.keys fieldPlans.zipWithIndex.partitionMap { - case (p: Plan.Configured[Fallible]) -> index => - recurse(p, value, F).asFieldValue(index, p.dest.tpe) - case plan -> index => + case plan -> index if sourceFields.isDefinedAt(index) => val fieldName = sourceFields(index) val fieldValue = value.accessFieldByName(fieldName).asExpr recurse(plan, fieldValue, F).asFieldValue(index, plan.dest.tpe) + case plan -> index => + recurse(plan, value, F).asFieldValue(index, plan.dest.tpe) } } 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 fd869f7b..afd1b581 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.Info + 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 061ffc3c..bbb8e98f 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 @@ -174,7 +174,7 @@ private[ducktape] object Plan { Quotes, Context ): Plan.Configured[F] = - (plan.source.tpe, plan.dest.tpe, conf.tpe) match { + (plan.source.tpe, plan.dest.tpe, conf.destTpe) match { case ('[src], '[dest], '[confTpe]) => val source = if instruction.side.isDest then Structure.Lazy.of[confTpe](plan.source.path) else plan.source val dest = if instruction.side.isSource then Structure.Lazy.of[confTpe](plan.dest.path) else plan.dest diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanConfigurer.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanConfigurer.scala index a2501752..19bc7232 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanConfigurer.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanConfigurer.scala @@ -425,56 +425,64 @@ private[ducktape] object PlanConfigurer { warnings: Accumulator[ConfigWarning], context: Context ) = { + //TODO: this is awful, refactor pls def isReplaceableBy(update: Configuration[F])(using Quotes) = - def checkDestTpe = update.tpe.repr <:< currentPlan.destPath.currentTpe.repr - def checkSourceTpe(srcTpe: Type[?]) = currentPlan.sourcePath.currentTpe.repr <:< srcTpe.repr - - //TODO: Make this nicer, this should also report which sourceType was actually expected as opposed to what was provided - update match - case Configuration.Const(value, tpe) => - checkDestTpe - case Configuration.CaseComputed(tpe, function) => - checkDestTpe - case Configuration.FieldComputed(tpe, function) => - checkDestTpe - case Configuration.FieldComputedDeep(tpe, sourceTpe, function) => - checkDestTpe && checkSourceTpe(sourceTpe) - case Configuration.FieldReplacement(source, name, tpe) => - checkDestTpe - case Configuration.FallibleConst(value, tpe) => - checkDestTpe - case Configuration.FallibleFieldComputed(tpe, function) => - checkDestTpe - case Configuration.FallibleFieldComputedDeep(tpe, sourceTpe, function) => - checkDestTpe && checkSourceTpe(sourceTpe) - case Configuration.FallibleCaseComputed(tpe, function) => - checkDestTpe - - if isReplaceableBy(config) then - val (path, _) = - Accumulator.append { - if instruction.side == Side.Dest then currentPlan.destPath -> instruction.side - else currentPlan.sourcePath -> instruction.side - } - Accumulator.appendAll { - ConfiguredCollector - .run(currentPlan, Nil) - .map(plan => ConfigWarning(plan.span, instruction.span, path)) - } - Plan.Configured.from(currentPlan, config, instruction) - else - Accumulator.append { - Plan.Error.from( - currentPlan, - ErrorMessage.InvalidConfiguration( - config.tpe, + def checkDestTpe = update.destTpe.repr <:< currentPlan.destPath.currentTpe.repr + def checkSourceTpe = + update.sourceTpe match + case None => true + case tpe: Type[?] => currentPlan.sourcePath.currentTpe.repr <:< tpe.repr + + Either + .cond( + checkDestTpe, + (), + ErrorMessage.InvalidConfigurationDestType( + config.destTpe, currentPlan.destPath.currentTpe, instruction.side, instruction.span - ), - None + ) ) - } + .flatMap { _ => + def tpe = config.sourceTpe match + case None => Type.of[Any] + case tpe: Type[?] => tpe + + Either.cond( + checkSourceTpe, + (), + ErrorMessage.InvalidConfigurationSourceType( + tpe, + currentPlan.sourcePath.currentTpe, + instruction.side, + instruction.span + ) + ) + } + + isReplaceableBy(config) match + case Left(value) => + Accumulator.append { + Plan.Error.from( + currentPlan, + value, + None + ) + } + case Right(value) => + val (path, _) = + Accumulator.append { + if instruction.side == Side.Dest then currentPlan.destPath -> instruction.side + else currentPlan.sourcePath -> instruction.side + } + Accumulator.appendAll { + ConfiguredCollector + .run(currentPlan, Nil) + .map(plan => ConfigWarning(plan.span, instruction.span, path)) + } + Plan.Configured.from(currentPlan, config, instruction) + } } diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanInterpreter.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanInterpreter.scala index 56652a21..4e30de18 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanInterpreter.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanInterpreter.scala @@ -22,50 +22,46 @@ private[ducktape] object PlanInterpreter { case Plan.Configured(_, _, config, _) => evaluateConfig(config, value) - case Plan.BetweenProducts(sourceTpe, destTpe, fieldPlans) => + case Plan.BetweenProducts(source, dest, fieldPlans) => val args = fieldPlans.map { - case (fieldName, p: Plan.Configured[Nothing]) if sourceTpe.fields.contains(fieldName) => - NamedArg(fieldName, recurse(p, value.accessFieldByName(fieldName).asExpr).asTerm) - case (fieldName, p: Plan.Configured[Nothing]) => - NamedArg(fieldName, recurse(p, value).asTerm) - - case (fieldName, plan) => + case (fieldName, plan) if source.fields.contains(fieldName) => val fieldValue = value.accessFieldByName(fieldName).asExpr NamedArg(fieldName, recurse(plan, fieldValue).asTerm) + case (fieldName, plan) => + NamedArg(fieldName, recurse(plan, value).asTerm) } - Constructor(destTpe.tpe.repr).appliedToArgs(args.toList).asExpr + Constructor(dest.tpe.repr).appliedToArgs(args.toList).asExpr case Plan.BetweenProductTuple(source, dest, plans) => val fields = source.fields.keys val args = plans.zipWithIndex.map { - //TODO: All other Product/Tuple or vice versa things need to handle passing in the value if they can - case (p: Plan.Configured[Nothing], _) => - recurse(p, value) - case (plan, index) => - val fieldName = fields(index) + case (plan, idx) if fields.isDefinedAt(idx) => + val fieldName = fields(idx) val fieldValue = value.accessFieldByName(fieldName).asExpr recurse(plan, fieldValue) + case (plan, _) => + recurse(plan, value) } Expr.ofTupleFromSeq(args.toSeq) case Plan.BetweenTupleProduct(source, dest, plans) => val args = plans.values.zipWithIndex.map { - case (p: Plan.Configured[Nothing], idx) => - recurse(p, value).asTerm - case (plan, idx) => + case (plan, idx) if source.elements.isDefinedAt(idx) => val elemValue = value.accesFieldByIndex(idx, source) recurse(plan, elemValue).asTerm + case (plan, _) => + recurse(plan, value).asTerm } Constructor(dest.tpe.repr).appliedToArgs(args.toList).asExpr case Plan.BetweenTuples(source, dest, plans) => val args = plans.zipWithIndex.map { - case (p: Plan.Configured[Nothing], idx) => - recurse(p, value) - case (plan, idx) => + case (plan, idx) if source.elements.isDefinedAt(idx) => val elemValue = value.accesFieldByIndex(idx, source) recurse(plan, elemValue) + case (plan, _) => + recurse(plan, value) } Expr.ofTupleFromSeq(args) @@ -80,23 +76,23 @@ private[ducktape] object PlanInterpreter { }.toList IfExpression(branches, '{ throw new RuntimeException("Unhandled case. This is most likely a bug in ducktape.") }).asExpr - case Plan.BetweenProductFunction(sourceTpe, destTpe, argPlans) => + case Plan.BetweenProductFunction(source, dest, argPlans) => val args = argPlans.map { - case (fieldName, p: Plan.Configured[Nothing]) => - recurse(p, value).asTerm - case (fieldName, plan) => + case (fieldName, plan) if source.fields.contains(fieldName) => val fieldValue = value.accessFieldByName(fieldName).asExpr recurse(plan, fieldValue).asTerm + case (fieldName, plan) => + recurse(plan, value).asTerm } - destTpe.function.appliedTo(args.toList) + dest.function.appliedTo(args.toList) case Plan.BetweenTupleFunction(source, dest, argPlans) => val args = argPlans.values.zipWithIndex.map { - case (p: Plan.Configured[Nothing], index) => - recurse(p, value).asTerm - case (plan, index) => + case (plan, index) if source.elements.isDefinedAt(index) => val fieldValue = value.accesFieldByIndex(index, source) recurse(plan, fieldValue).asTerm + case (plan, index) => + recurse(plan, value).asTerm } dest.function.appliedTo(args.toList) From c05568f60e3b797e50938769de6bd0252ba7d797 Mon Sep 17 00:00:00 2001 From: Aleksander Rainko Date: Thu, 10 Oct 2024 22:22:19 +0200 Subject: [PATCH 05/11] add initial set of total transformation tests --- .../io/github/arainko/ducktape/Mode.scala | 15 -- .../ducktape/internal/ConfigParser.scala | 7 +- .../ducktape/internal/PlanConfigurer.scala | 55 +++---- .../ducktape/internal/extensions.scala | 20 +++ .../arainko/ducktape/DucktapeSuite.scala | 13 ++ .../total/NestedConfigurationSuite.scala | 138 ++++++++++++++++++ 6 files changed, 198 insertions(+), 50 deletions(-) diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/Mode.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/Mode.scala index e88360a8..7b46fd6e 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/Mode.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/Mode.scala @@ -157,18 +157,3 @@ object Mode { def either[E]: Mode.FailFast.Either[E] = Mode.FailFast.Either[E] } } - -object test2 { - // import io.github.arainko.ducktape.* - - private case class Source(opt: Option[Level1]) - private case class Level1(int: Int) - - internal.CodePrinter.code: - locally { - Source(Some(Level1(2))) - .into[Source] - .transform(Field.computedDeep(_.opt.element.int, (int: Any) => int.toString.toInt + 1)) - } - -} diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/ConfigParser.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/ConfigParser.scala index 664864d5..ae63d3da 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/ConfigParser.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/ConfigParser.scala @@ -15,10 +15,9 @@ private[ducktape] object ConfigParser { def fallible[F[+x]: Type] = NonEmptyList(Total, PossiblyFallible[F]) - def combine[F <: Fallible](parsers: NonEmptyList[ConfigParser[F]])(using - Quotes, - Context - ): PartialFunction[quotes.reflect.Term, Instruction[F]] = + def combine[F <: Fallible]( + parsers: NonEmptyList[ConfigParser[F]] + )(using Quotes, Context): PartialFunction[quotes.reflect.Term, Instruction[F]] = parsers.map(_.apply).reduceLeft(_ orElse _) object Total extends ConfigParser[Nothing] { diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanConfigurer.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanConfigurer.scala index 19bc7232..f6a4e3f4 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanConfigurer.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanConfigurer.scala @@ -425,44 +425,37 @@ private[ducktape] object PlanConfigurer { warnings: Accumulator[ConfigWarning], context: Context ) = { - //TODO: this is awful, refactor pls - def isReplaceableBy(update: Configuration[F])(using Quotes) = - def checkDestTpe = update.destTpe.repr <:< currentPlan.destPath.currentTpe.repr - def checkSourceTpe = - update.sourceTpe match - case None => true - case tpe: Type[?] => currentPlan.sourcePath.currentTpe.repr <:< tpe.repr + def isReplaceableBy(update: Configuration[F])(using Quotes) = { + def checkDestTpe = + Either + .cond( + update.destTpe.repr <:< currentPlan.destPath.currentTpe.repr, + (), + ErrorMessage.InvalidConfigurationDestType( + config.destTpe, + currentPlan.destPath.currentTpe, + instruction.side, + instruction.span + ) + ) - Either - .cond( - checkDestTpe, + def checkSourceTpe = + Either.cond( + update.sourceTpe.fold(true, tpe => currentPlan.sourcePath.currentTpe.repr <:< tpe.repr), (), - ErrorMessage.InvalidConfigurationDestType( - config.destTpe, - currentPlan.destPath.currentTpe, + ErrorMessage.InvalidConfigurationSourceType( + config.sourceTpe.getOrElse(Type.of[Any]), + currentPlan.sourcePath.currentTpe, // TODO: Check if .currentTpe or rather sourcePath.last should be used here instruction.side, instruction.span ) ) - .flatMap { _ => - def tpe = config.sourceTpe match - case None => Type.of[Any] - case tpe: Type[?] => tpe - Either.cond( - checkSourceTpe, - (), - ErrorMessage.InvalidConfigurationSourceType( - tpe, - currentPlan.sourcePath.currentTpe, - instruction.side, - instruction.span - ) - ) - } + checkSourceTpe.zipRight(checkDestTpe) + } - isReplaceableBy(config) match - case Left(value) => + isReplaceableBy(config) match { + case Left(value) => Accumulator.append { Plan.Error.from( currentPlan, @@ -482,7 +475,7 @@ private[ducktape] object PlanConfigurer { .map(plan => ConfigWarning(plan.span, instruction.span, path)) } Plan.Configured.from(currentPlan, config, instruction) - + } } } diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/extensions.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/extensions.scala index 81785ea6..4fcf9243 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/extensions.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/extensions.scala @@ -30,3 +30,23 @@ extension (expr: Expr[Any]) { } } + +extension [A, B](self: Either[A, B]) { + private[ducktape] inline def zipRight[AA >: A, C](inline that: Either[AA, C]): Either[AA, C] = + self.flatMap(_ => that) +} + +extension [A](self: A | None.type) { + private[ducktape] inline def getOrElse[AA >: A](inline fallback: AA): AA = + self.fold(fallback, a => a) + + private[ducktape] inline def fold[B](inline caseNone: B, inline caseA: A => B): B = + self match + case None => caseNone + case a: A => caseA(a) + + // private[ducktape] inline def map[B](inline f: A => B): B | None.type = self.fold(None, f) + + + +} diff --git a/ducktape/src/test/scala/io/github/arainko/ducktape/DucktapeSuite.scala b/ducktape/src/test/scala/io/github/arainko/ducktape/DucktapeSuite.scala index 3c1649cc..4bea382e 100644 --- a/ducktape/src/test/scala/io/github/arainko/ducktape/DucktapeSuite.scala +++ b/ducktape/src/test/scala/io/github/arainko/ducktape/DucktapeSuite.scala @@ -31,6 +31,19 @@ trait DucktapeSuite extends FunSuite { Transformer.define[A, B].build().transform(source) )(expected) + // transparent inline def assertTransformsVia[A, B, Func, Args <: FunctionArguments]( + // source: A, + // inline func: A => B, + // inline func1: DefinitionViaBuilder.PartiallyApplied[A] => DefinitionViaBuilder[A, B, Func, Args], + // inline func2: A => AppliedViaBuilder[A, B, Func, Args], + // expected: B + // )(using Location) = + // assertEachEquals( + // source.via(func), + // func1(DefinitionViaBuilder.create[A]).build().transform(source), + // func2(source).transform() + // )(expected) + inline def assertTransformsFallible[F[+x], M <: Mode[F], A, B](using M)(source: A, expected: F[B])(using loc: Location) = assertEachEquals( source.fallibleTo[B], diff --git a/ducktape/src/test/scala/io/github/arainko/ducktape/total/NestedConfigurationSuite.scala b/ducktape/src/test/scala/io/github/arainko/ducktape/total/NestedConfigurationSuite.scala index 2eaa9816..e4c71b7b 100644 --- a/ducktape/src/test/scala/io/github/arainko/ducktape/total/NestedConfigurationSuite.scala +++ b/ducktape/src/test/scala/io/github/arainko/ducktape/total/NestedConfigurationSuite.scala @@ -790,4 +790,142 @@ class NestedConfigurationSuite extends DucktapeSuite { .transform(source) )(expected) } + + test("Field.computedDeep works in deeply nested case classes") { + case class SourceToplevel1(level1: SourceLevel1) + case class SourceLevel1(level2: SourceLevel2) + case class SourceLevel2(int: Int) + + case class DestToplevel1(level1: DestLevel1) + case class DestLevel1(level2: DestLevel2) + case class DestLevel2(int: Long) + + val source = SourceToplevel1(SourceLevel1(SourceLevel2(1))) + val expected = DestToplevel1(DestLevel1(DestLevel2(11))) + + assertTransformsConfigured(source, expected)( + Field.computedDeep(_.level1.level2.int, (int: Int) => int.toLong + 10) + ) + + assertEachEquals( + source + .intoVia(DestToplevel1.apply) + .transform(Field.computedDeep(_.level1.level2.int, (int: Int) => int.toLong + 10)), + Transformer + .defineVia[SourceToplevel1](DestToplevel1.apply) + .build(Field.computedDeep(_.level1.level2.int, (int: Int) => int.toLong + 10)) + .transform(source) + )(expected) + + } + + test("Field.computedDeep works with deeply nested tuples") { + val source = Tuple1(Tuple1(Tuple1(1))) + + val expected = Tuple1(Tuple1(Tuple1(11L))) + + assertTransformsConfigured(source, expected)( + Field.computedDeep(_.apply(0).apply(0).apply(0), (int: Int) => int.toLong + 10) + ) + + assertEachEquals( + source + .intoVia(Tuple1.apply[Tuple1[Tuple1[Long]]]) + .transform(Field.computedDeep(_._1._1._1, (int: Int) => int.toLong + 10)), + Transformer + .defineVia[Tuple1[Tuple1[Tuple1[Int]]]](Tuple1.apply[Tuple1[Tuple1[Long]]]) + .build(Field.computedDeep(_._1._1._1, (int: Int) => int.toLong + 10)) + .transform(source) + )(expected) + } + + test("Field.computedDeep works with Options") { + case class SourceToplevel1(level1: Option[SourceLevel1]) + case class SourceLevel1(level2: Option[SourceLevel2]) + case class SourceLevel2(level3: SourceLevel3) + case class SourceLevel3(int: Int) + + case class DestToplevel1(level1: Option[DestLevel1]) + case class DestLevel1(level2: Option[DestLevel2]) + case class DestLevel2(level3: Option[DestLevel3]) + case class DestLevel3(int: Long) + + val source = SourceToplevel1(Some(SourceLevel1(Some(SourceLevel2(SourceLevel3(1)))))) + val expected = DestToplevel1(Some(DestLevel1(Some(DestLevel2(Some(DestLevel3(11))))))) + + assertTransformsConfigured(source, expected)( + Field.computedDeep(_.level1.element.level2.element.level3.element.int, (int: Int) => int.toLong + 10) + ) + + assertEachEquals( + source + .intoVia(DestToplevel1.apply) + .transform( + Field.computedDeep(_.level1.element.level2.element.level3.element.int, (int: Int) => int.toLong + 10) + ), + Transformer + .defineVia[SourceToplevel1](DestToplevel1.apply) + .build( + Field.computedDeep(_.level1.element.level2.element.level3.element.int, (int: Int) => int.toLong + 10) + ) + .transform(source) + )(expected) + + } + + test("Field.computedDeep works with collections") { + case class SourceToplevel1(level1: Vector[SourceLevel1]) + case class SourceLevel1(level2: Vector[SourceLevel2]) + case class SourceLevel2(level3: Vector[SourceLevel3]) + case class SourceLevel3(int: Int) + + case class DestToplevel1(level1: List[DestLevel1]) + case class DestLevel1(level2: List[DestLevel2]) + case class DestLevel2(level3: List[DestLevel3]) + case class DestLevel3(int: Long) + + val source = SourceToplevel1(Vector(SourceLevel1(Vector(SourceLevel2(Vector(SourceLevel3(1))))))) + val expected = DestToplevel1(List(DestLevel1(List(DestLevel2(List(DestLevel3(11))))))) + + assertTransformsConfigured(source, expected)( + Field.computedDeep(_.level1.element.level2.element.level3.element.int, (int: Int) => int.toLong + 10) + ) + + assertEachEquals( + source + .intoVia(DestToplevel1.apply) + .transform( + Field.computedDeep(_.level1.element.level2.element.level3.element.int, (int: Int) => int.toLong + 10) + ), + Transformer + .defineVia[SourceToplevel1](DestToplevel1.apply) + .build( + Field.computedDeep(_.level1.element.level2.element.level3.element.int, (int: Int) => int.toLong + 10) + ) + .transform(source) + )(expected) + } + + test("Field.computedDeep works with coproducts".ignore) {} + + test("Field.computedDeep reports the right source type if the one given to it is wrong".ignore) {} + + // TODO: Think this through, should this error out? This currently picks up the 'nearest' source field to the dest one + test("Field.computedDeep works correctly when a field on the same level is missing in the Source".ignore) { + case class SourceToplevel1(level1: SourceLevel1) + case class SourceLevel1(level2: SourceLevel2) + case class SourceLevel2(int: Int) + + case class DestToplevel1(level1: DestLevel1) + case class DestLevel1(level2: DestLevel2) + case class DestLevel2(int: Int, extra: String) + + val source = SourceToplevel1(SourceLevel1(SourceLevel2(1))) + val expected = DestToplevel1(DestLevel1(DestLevel2(1, "1CONF"))) + + assertTransformsConfigured(source, expected)( + Field.computedDeep(_.level1.level2.extra, (a: SourceLevel2) => a.int.toString()) + ) + } + } From 2522a6c16ff8f760bcc8cca0ab810a01754c819a Mon Sep 17 00:00:00 2001 From: arainko Date: Sat, 12 Oct 2024 18:44:09 +0200 Subject: [PATCH 06/11] add coprod test --- .../total/NestedConfigurationSuite.scala | 55 ++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/ducktape/src/test/scala/io/github/arainko/ducktape/total/NestedConfigurationSuite.scala b/ducktape/src/test/scala/io/github/arainko/ducktape/total/NestedConfigurationSuite.scala index e4c71b7b..84eb5ed1 100644 --- a/ducktape/src/test/scala/io/github/arainko/ducktape/total/NestedConfigurationSuite.scala +++ b/ducktape/src/test/scala/io/github/arainko/ducktape/total/NestedConfigurationSuite.scala @@ -906,7 +906,60 @@ class NestedConfigurationSuite extends DucktapeSuite { )(expected) } - test("Field.computedDeep works with coproducts".ignore) {} + test("Field.computedDeep works with coproducts") { + enum SourceToplevel1 { + case Level1(level2: SourceLevel2) + } + + enum SourceLevel2 { + case Level2(level3: SourceLevel3) + } + + enum SourceLevel3 { + case One(int: Int) + case Two(str: String) + } + + enum DestToplevel1 { + case Level1(level2: DestLevel2) + } + + enum DestLevel2 { + case Level2(level3: DestLevel3) + } + + enum DestLevel3 { + case One(int: Long) + case Two(str: String) + } + + val source = SourceToplevel1.Level1(SourceLevel2.Level2(SourceLevel3.One(1))) + val expected = DestToplevel1.Level1(DestLevel2.Level2(DestLevel3.One(6))) + + assertTransformsConfigured(source, expected)( + Field.computedDeep(_.at[DestToplevel1.Level1].level2.at[DestLevel2.Level2].level3.at[DestLevel3.One], (a: SourceLevel3.One) => ???) + ) + + // assertEachEquals( + // source + // .into[DestToplevel1] + // .transform( + // Case.computed( + // _.at[SourceToplevel1.Level1].level2.at[SourceLevel2.Level2].level3.at[SourceLevel3.Extra], + // extra => DestLevel3.One(extra.int + 5) + // ) + // ), + // Transformer + // .define[SourceToplevel1, DestToplevel1] + // .build( + // Case.computed( + // _.at[SourceToplevel1.Level1].level2.at[SourceLevel2.Level2].level3.at[SourceLevel3.Extra], + // extra => DestLevel3.One(extra.int + 5) + // ) + // ) + // .transform(source) + // )(expected) + } test("Field.computedDeep reports the right source type if the one given to it is wrong".ignore) {} From 9f005e33f6880edba7a1d26338d3305c36036d8c Mon Sep 17 00:00:00 2001 From: arainko Date: Fri, 18 Oct 2024 20:23:52 +0200 Subject: [PATCH 07/11] add a tet that reporduces a bug --- .../arainko/ducktape/internal/Path.scala | 3 + .../ducktape/internal/PlanConfigurer.scala | 4 +- .../ducktape/internal/PlanInterpreter.scala | 2 +- .../total/NestedConfigurationSuite.scala | 64 +++++++++---------- 4 files changed, 38 insertions(+), 35 deletions(-) diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Path.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Path.scala index 5e1cf9fd..a102e4c7 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Path.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Path.scala @@ -12,6 +12,9 @@ private[ducktape] final case class Path(root: Type[?], segments: Vector[Path.Seg def prepended(segment: Path.Segment): Path = self.copy(segments = segments.prepended(segment)) + def narrowedCurrentTpe(using Quotes): Type[?] = + segments.lastOption.fold(root)(_.tpe) + // deliberately use something that requires a total function so that when a new Path.Segment is declared // it's not forgotten about def currentTpe(using Quotes): Type[?] = { diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanConfigurer.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanConfigurer.scala index f6a4e3f4..b738ce92 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanConfigurer.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanConfigurer.scala @@ -441,11 +441,11 @@ private[ducktape] object PlanConfigurer { def checkSourceTpe = Either.cond( - update.sourceTpe.fold(true, tpe => currentPlan.sourcePath.currentTpe.repr <:< tpe.repr), + update.sourceTpe.fold(true, tpe => currentPlan.sourcePath.narrowedCurrentTpe.repr <:< tpe.repr), (), ErrorMessage.InvalidConfigurationSourceType( config.sourceTpe.getOrElse(Type.of[Any]), - currentPlan.sourcePath.currentTpe, // TODO: Check if .currentTpe or rather sourcePath.last should be used here + currentPlan.sourcePath.narrowedCurrentTpe, // TODO: Check if .currentTpe or rather sourcePath.last should be used here instruction.side, instruction.span ) diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanInterpreter.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanInterpreter.scala index 4e30de18..5d8b6b7c 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanInterpreter.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanInterpreter.scala @@ -160,7 +160,7 @@ private[ducktape] object PlanInterpreter { case Configuration.FieldComputed(_, function) => '{ $function.apply($toplevelValue) } case Configuration.FieldComputedDeep(tpe, sourceTpe, function) => - '{ $function.apply($value) } + /*Expr.betaReduce(*/'{ $function.apply($value) }/*)*/ case Configuration.FieldReplacement(source, name, tpe) => source.accessFieldByName(name).asExpr } diff --git a/ducktape/src/test/scala/io/github/arainko/ducktape/total/NestedConfigurationSuite.scala b/ducktape/src/test/scala/io/github/arainko/ducktape/total/NestedConfigurationSuite.scala index 84eb5ed1..20521391 100644 --- a/ducktape/src/test/scala/io/github/arainko/ducktape/total/NestedConfigurationSuite.scala +++ b/ducktape/src/test/scala/io/github/arainko/ducktape/total/NestedConfigurationSuite.scala @@ -907,38 +907,38 @@ class NestedConfigurationSuite extends DucktapeSuite { } test("Field.computedDeep works with coproducts") { - enum SourceToplevel1 { - case Level1(level2: SourceLevel2) - } - - enum SourceLevel2 { - case Level2(level3: SourceLevel3) - } - - enum SourceLevel3 { - case One(int: Int) - case Two(str: String) - } - - enum DestToplevel1 { - case Level1(level2: DestLevel2) - } - - enum DestLevel2 { - case Level2(level3: DestLevel3) - } - - enum DestLevel3 { - case One(int: Long) - case Two(str: String) - } - - val source = SourceToplevel1.Level1(SourceLevel2.Level2(SourceLevel3.One(1))) - val expected = DestToplevel1.Level1(DestLevel2.Level2(DestLevel3.One(6))) - - assertTransformsConfigured(source, expected)( - Field.computedDeep(_.at[DestToplevel1.Level1].level2.at[DestLevel2.Level2].level3.at[DestLevel3.One], (a: SourceLevel3.One) => ???) - ) + // enum SourceToplevel1 { + // case Level1(level2: SourceLevel2) + // } + + // enum SourceLevel2 { + // case Level2(level3: SourceLevel3) + // } + + // enum SourceLevel3 { + // case One(int: Int) + // case Two(str: String) + // } + + // enum DestToplevel1 { + // case Level1(level2: DestLevel2) + // } + + // enum DestLevel2 { + // case Level2(level3: DestLevel3) + // } + + // enum DestLevel3 { + // case One(int: Long) + // case Two(str: String) + // } + + // val source = SourceToplevel1.Level1(SourceLevel2.Level2(SourceLevel3.One(1))) + // val expected = DestToplevel1.Level1(DestLevel2.Level2(DestLevel3.One(6))) + + // assertTransformsConfigured(source, expected)( + // Field.computedDeep(_.at[DestToplevel1.Level1].level2.at[DestLevel2.Level2].level3.at[DestLevel3.One], (a: SourceLevel3.One) => ???) + // ) // assertEachEquals( // source From 5c56f4f6072113fda9cab6ac2b50d6b29cd7a10b Mon Sep 17 00:00:00 2001 From: arainko Date: Fri, 18 Oct 2024 22:05:54 +0200 Subject: [PATCH 08/11] testin' --- ...AccumulatingNestedConfigurationSuite.scala | 160 ++++++++++++++++++ .../total/NestedConfigurationSuite.scala | 120 +++++++------ 2 files changed, 227 insertions(+), 53 deletions(-) diff --git a/ducktape/src/test/scala/io/github/arainko/ducktape/fallible/accumulating/AccumulatingNestedConfigurationSuite.scala b/ducktape/src/test/scala/io/github/arainko/ducktape/fallible/accumulating/AccumulatingNestedConfigurationSuite.scala index a4a0a7ed..d3ad4d48 100644 --- a/ducktape/src/test/scala/io/github/arainko/ducktape/fallible/accumulating/AccumulatingNestedConfigurationSuite.scala +++ b/ducktape/src/test/scala/io/github/arainko/ducktape/fallible/accumulating/AccumulatingNestedConfigurationSuite.scala @@ -881,4 +881,164 @@ class AccumulatingNestedConfigurationSuite extends DucktapeSuite { .transform(source) )(F.pure(expected)) } + + test("Field.computedDeep works in deeply nested case classes") { + case class SourceToplevel1(level1: SourceLevel1) + case class SourceLevel1(level2: SourceLevel2) + case class SourceLevel2(int: Int) + + case class DestToplevel1(level1: DestLevel1) + case class DestLevel1(level2: DestLevel2) + case class DestLevel2(int: Long) + + val source = SourceToplevel1(SourceLevel1(SourceLevel2(1))) + val expected = DestToplevel1(DestLevel1(DestLevel2(11))) + + assertTransformsConfigured(source, expected)( + Field.computedDeep(_.level1.level2.int, (int: Int) => int.toLong + 10) + ) + + assertEachEquals( + source + .intoVia(DestToplevel1.apply) + .transform(Field.computedDeep(_.level1.level2.int, (int: Int) => int.toLong + 10)), + Transformer + .defineVia[SourceToplevel1](DestToplevel1.apply) + .build(Field.computedDeep(_.level1.level2.int, (int: Int) => int.toLong + 10)) + .transform(source) + )(expected) + + } + + test("Field.computedDeep works with deeply nested tuples") { + val source = Tuple1(Tuple1(Tuple1(1))) + + val expected = Tuple1(Tuple1(Tuple1(11L))) + + assertTransformsConfigured(source, expected)( + Field.computedDeep(_.apply(0).apply(0).apply(0), (int: Int) => int.toLong + 10) + ) + + assertEachEquals( + source + .intoVia(Tuple1.apply[Tuple1[Tuple1[Long]]]) + .transform(Field.computedDeep(_._1._1._1, (int: Int) => int.toLong + 10)), + Transformer + .defineVia[Tuple1[Tuple1[Tuple1[Int]]]](Tuple1.apply[Tuple1[Tuple1[Long]]]) + .build(Field.computedDeep(_._1._1._1, (int: Int) => int.toLong + 10)) + .transform(source) + )(expected) + } + + test("Field.computedDeep works with Options") { + case class SourceToplevel1(level1: Option[SourceLevel1]) + case class SourceLevel1(level2: Option[SourceLevel2]) + case class SourceLevel2(level3: SourceLevel3) + case class SourceLevel3(int: Int) + + case class DestToplevel1(level1: Option[DestLevel1]) + case class DestLevel1(level2: Option[DestLevel2]) + case class DestLevel2(level3: Option[DestLevel3]) + case class DestLevel3(int: Long) + + val source = SourceToplevel1(Some(SourceLevel1(Some(SourceLevel2(SourceLevel3(1)))))) + val expected = DestToplevel1(Some(DestLevel1(Some(DestLevel2(Some(DestLevel3(11))))))) + + assertTransformsConfigured(source, expected)( + Field.computedDeep(_.level1.element.level2.element.level3.element.int, (int: Int) => int.toLong + 10) + ) + + assertEachEquals( + source + .intoVia(DestToplevel1.apply) + .transform( + Field.computedDeep(_.level1.element.level2.element.level3.element.int, (int: Int) => int.toLong + 10) + ), + Transformer + .defineVia[SourceToplevel1](DestToplevel1.apply) + .build( + Field.computedDeep(_.level1.element.level2.element.level3.element.int, (int: Int) => int.toLong + 10) + ) + .transform(source) + )(expected) + + } + + test("Field.computedDeep works with collections") { + case class SourceToplevel1(level1: Vector[SourceLevel1]) + case class SourceLevel1(level2: Vector[SourceLevel2]) + case class SourceLevel2(level3: Vector[SourceLevel3]) + case class SourceLevel3(int: Int) + + case class DestToplevel1(level1: List[DestLevel1]) + case class DestLevel1(level2: List[DestLevel2]) + case class DestLevel2(level3: List[DestLevel3]) + case class DestLevel3(int: Long) + + val source = SourceToplevel1(Vector(SourceLevel1(Vector(SourceLevel2(Vector(SourceLevel3(1))))))) + val expected = DestToplevel1(List(DestLevel1(List(DestLevel2(List(DestLevel3(11))))))) + + assertTransformsConfigured(source, expected)( + Field.computedDeep(_.level1.element.level2.element.level3.element.int, (int: Int) => int.toLong + 10) + ) + + assertEachEquals( + source + .intoVia(DestToplevel1.apply) + .transform( + Field.computedDeep(_.level1.element.level2.element.level3.element.int, (int: Int) => int.toLong + 10) + ), + Transformer + .defineVia[SourceToplevel1](DestToplevel1.apply) + .build( + Field.computedDeep(_.level1.element.level2.element.level3.element.int, (int: Int) => int.toLong + 10) + ) + .transform(source) + )(expected) + } + + test("Field.computedDeep works with coproducts") { + enum SourceToplevel1 { + case Level1(level2: SourceLevel2) + } + + enum SourceLevel2 { + case Level2(level3: SourceLevel3) + } + + enum SourceLevel3 { + case One(int: Int) + case Two(str: String) + } + + enum DestToplevel1 { + case Level1(level2: DestLevel2) + } + + enum DestLevel2 { + case Level2(level3: DestLevel3) + } + + enum DestLevel3 { + case One(int: Long) + case Two(str: String) + } + + val source = SourceToplevel1.Level1(SourceLevel2.Level2(SourceLevel3.One(1))) + val expected = DestToplevel1.Level1(DestLevel2.Level2(DestLevel3.One(6))) + + assertTransformsConfigured(source, expected)( + Field.computedDeep( + _.at[DestToplevel1.Level1].level2.at[DestLevel2.Level2].level3.at[DestLevel3.One], + (a: SourceLevel3.One) => DestLevel3.One(a.int.toLong + 5) + ) + ) + + assertTransformsConfigured(source, expected)( + Field.computedDeep( + _.at[DestToplevel1.Level1].level2.at[DestLevel2.Level2].level3.at[DestLevel3.One].int, + (a: Int) => a + 5L + ) + ) + } } diff --git a/ducktape/src/test/scala/io/github/arainko/ducktape/total/NestedConfigurationSuite.scala b/ducktape/src/test/scala/io/github/arainko/ducktape/total/NestedConfigurationSuite.scala index 20521391..46642e76 100644 --- a/ducktape/src/test/scala/io/github/arainko/ducktape/total/NestedConfigurationSuite.scala +++ b/ducktape/src/test/scala/io/github/arainko/ducktape/total/NestedConfigurationSuite.scala @@ -907,61 +907,75 @@ class NestedConfigurationSuite extends DucktapeSuite { } test("Field.computedDeep works with coproducts") { - // enum SourceToplevel1 { - // case Level1(level2: SourceLevel2) - // } - - // enum SourceLevel2 { - // case Level2(level3: SourceLevel3) - // } - - // enum SourceLevel3 { - // case One(int: Int) - // case Two(str: String) - // } - - // enum DestToplevel1 { - // case Level1(level2: DestLevel2) - // } - - // enum DestLevel2 { - // case Level2(level3: DestLevel3) - // } - - // enum DestLevel3 { - // case One(int: Long) - // case Two(str: String) - // } - - // val source = SourceToplevel1.Level1(SourceLevel2.Level2(SourceLevel3.One(1))) - // val expected = DestToplevel1.Level1(DestLevel2.Level2(DestLevel3.One(6))) - - // assertTransformsConfigured(source, expected)( - // Field.computedDeep(_.at[DestToplevel1.Level1].level2.at[DestLevel2.Level2].level3.at[DestLevel3.One], (a: SourceLevel3.One) => ???) - // ) - - // assertEachEquals( - // source - // .into[DestToplevel1] - // .transform( - // Case.computed( - // _.at[SourceToplevel1.Level1].level2.at[SourceLevel2.Level2].level3.at[SourceLevel3.Extra], - // extra => DestLevel3.One(extra.int + 5) - // ) - // ), - // Transformer - // .define[SourceToplevel1, DestToplevel1] - // .build( - // Case.computed( - // _.at[SourceToplevel1.Level1].level2.at[SourceLevel2.Level2].level3.at[SourceLevel3.Extra], - // extra => DestLevel3.One(extra.int + 5) - // ) - // ) - // .transform(source) - // )(expected) + enum SourceToplevel1 { + case Level1(level2: SourceLevel2) + } + + enum SourceLevel2 { + case Level2(level3: SourceLevel3) + } + + enum SourceLevel3 { + case One(int: Int) + case Two(str: String) + } + + enum DestToplevel1 { + case Level1(level2: DestLevel2) + } + + enum DestLevel2 { + case Level2(level3: DestLevel3) + } + + enum DestLevel3 { + case One(int: Long) + case Two(str: String) + } + + val source = SourceToplevel1.Level1(SourceLevel2.Level2(SourceLevel3.One(1))) + val expected = DestToplevel1.Level1(DestLevel2.Level2(DestLevel3.One(6))) + + assertTransformsConfigured(source, expected)( + Field.computedDeep( + _.at[DestToplevel1.Level1].level2.at[DestLevel2.Level2].level3.at[DestLevel3.One], + (a: SourceLevel3.One) => DestLevel3.One(a.int.toLong + 5) + ) + ) + + assertTransformsConfigured(source, expected)( + Field.computedDeep( + _.at[DestToplevel1.Level1].level2.at[DestLevel2.Level2].level3.at[DestLevel3.One].int, + (a: Int) => a + 5L + ) + ) } - test("Field.computedDeep reports the right source type if the one given to it is wrong".ignore) {} + test("Field.computedDeep reports the right source type if the one given to it is wrong") { + case class SourceToplevel1(level1: Vector[SourceLevel1]) + case class SourceLevel1(level2: Vector[SourceLevel2]) + case class SourceLevel2(level3: Vector[SourceLevel3]) + case class SourceLevel3(int: Int) + + case class DestToplevel1(level1: List[DestLevel1]) + case class DestLevel1(level2: List[DestLevel2]) + case class DestLevel2(level3: List[DestLevel3]) + case class DestLevel3(int: Long) + + val source = SourceToplevel1(Vector(SourceLevel1(Vector(SourceLevel2(Vector(SourceLevel3(1))))))) + val expected = DestToplevel1(List(DestLevel1(List(DestLevel2(List(DestLevel3(11))))))) + + assertFailsToCompileWith { + """ + source.into[DestToplevel1].transform( + Field.computedDeep(_.level1.element.level2.element.level3.element.int, (int: String) => int.toLong + 10L) + ) + """ + }( + "Configuration is not valid since the provided source type (java.lang.String) is not a supertype of scala.Int @ DestToplevel1.level1.element.level2.element.level3.element.int", + "Couldn't build a transformation plan between scala.Int and scala.Long @ DestToplevel1.level1.element.level2.element.level3.element.int" + ) + }: @nowarn // TODO: Think this through, should this error out? This currently picks up the 'nearest' source field to the dest one test("Field.computedDeep works correctly when a field on the same level is missing in the Source".ignore) { From 942fa7bd9478f77e12d5b74b516e8476d6d00c85 Mon Sep 17 00:00:00 2001 From: Aleksander Rainko Date: Fri, 18 Oct 2024 23:06:54 +0200 Subject: [PATCH 09/11] fallible transformation tests --- ...AccumulatingNestedConfigurationSuite.scala | 137 ++++++++++-------- 1 file changed, 73 insertions(+), 64 deletions(-) diff --git a/ducktape/src/test/scala/io/github/arainko/ducktape/fallible/accumulating/AccumulatingNestedConfigurationSuite.scala b/ducktape/src/test/scala/io/github/arainko/ducktape/fallible/accumulating/AccumulatingNestedConfigurationSuite.scala index d3ad4d48..53e8e617 100644 --- a/ducktape/src/test/scala/io/github/arainko/ducktape/fallible/accumulating/AccumulatingNestedConfigurationSuite.scala +++ b/ducktape/src/test/scala/io/github/arainko/ducktape/fallible/accumulating/AccumulatingNestedConfigurationSuite.scala @@ -882,52 +882,56 @@ class AccumulatingNestedConfigurationSuite extends DucktapeSuite { )(F.pure(expected)) } - test("Field.computedDeep works in deeply nested case classes") { + test("Field.fallibleComputedDeep works in deeply nested case classes") { case class SourceToplevel1(level1: SourceLevel1) case class SourceLevel1(level2: SourceLevel2) case class SourceLevel2(int: Int) case class DestToplevel1(level1: DestLevel1) case class DestLevel1(level2: DestLevel2) - case class DestLevel2(int: Long) + case class DestLevel2(int: Positive) val source = SourceToplevel1(SourceLevel1(SourceLevel2(1))) - val expected = DestToplevel1(DestLevel1(DestLevel2(11))) + val expected = DestToplevel1(DestLevel1(DestLevel2(Positive(11)))) - assertTransformsConfigured(source, expected)( - Field.computedDeep(_.level1.level2.int, (int: Int) => int.toLong + 10) + assertTransformsFallibleConfigured(source, F.pure(expected))( + Field.fallibleComputedDeep(_.level1.level2.int, (int: Int) => fallibleComputation(int + 10)) ) assertEachEquals( source .intoVia(DestToplevel1.apply) - .transform(Field.computedDeep(_.level1.level2.int, (int: Int) => int.toLong + 10)), + .fallible + .transform(Field.fallibleComputedDeep(_.level1.level2.int, (int: Int) => fallibleComputation(int + 10))), Transformer .defineVia[SourceToplevel1](DestToplevel1.apply) - .build(Field.computedDeep(_.level1.level2.int, (int: Int) => int.toLong + 10)) + .fallible + .build(Field.fallibleComputedDeep(_.level1.level2.int, (int: Int) => fallibleComputation(int + 10))) .transform(source) - )(expected) + )(F.pure(expected)) } - test("Field.computedDeep works with deeply nested tuples") { + test("Field.fallibleComputedDeep works with deeply nested tuples") { val source = Tuple1(Tuple1(Tuple1(1))) - val expected = Tuple1(Tuple1(Tuple1(11L))) + val expected = Tuple1(Tuple1(Tuple1(Positive(11)))) - assertTransformsConfigured(source, expected)( - Field.computedDeep(_.apply(0).apply(0).apply(0), (int: Int) => int.toLong + 10) + assertTransformsFallibleConfigured(source, F.pure(expected))( + Field.fallibleComputedDeep(_.apply(0).apply(0).apply(0), (int: Int) => fallibleComputation(int + 10)) ) assertEachEquals( source - .intoVia(Tuple1.apply[Tuple1[Tuple1[Long]]]) - .transform(Field.computedDeep(_._1._1._1, (int: Int) => int.toLong + 10)), + .intoVia(Tuple1.apply[Tuple1[Tuple1[Positive]]]) + .fallible + .transform(Field.fallibleComputedDeep(_._1._1._1, (int: Int) => fallibleComputation(int + 10))), Transformer - .defineVia[Tuple1[Tuple1[Tuple1[Int]]]](Tuple1.apply[Tuple1[Tuple1[Long]]]) - .build(Field.computedDeep(_._1._1._1, (int: Int) => int.toLong + 10)) + .defineVia[Tuple1[Tuple1[Tuple1[Int]]]](Tuple1.apply[Tuple1[Tuple1[Positive]]]) + .fallible + .build(Field.fallibleComputedDeep(_._1._1._1, (int: Int) => fallibleComputation(int + 10))) .transform(source) - )(expected) + )(F.pure(expected)) } test("Field.computedDeep works with Options") { @@ -939,63 +943,68 @@ class AccumulatingNestedConfigurationSuite extends DucktapeSuite { case class DestToplevel1(level1: Option[DestLevel1]) case class DestLevel1(level2: Option[DestLevel2]) case class DestLevel2(level3: Option[DestLevel3]) - case class DestLevel3(int: Long) + case class DestLevel3(int: Positive) val source = SourceToplevel1(Some(SourceLevel1(Some(SourceLevel2(SourceLevel3(1)))))) - val expected = DestToplevel1(Some(DestLevel1(Some(DestLevel2(Some(DestLevel3(11))))))) + val expected = DestToplevel1(Some(DestLevel1(Some(DestLevel2(Some(DestLevel3(Positive(11)))))))) - assertTransformsConfigured(source, expected)( - Field.computedDeep(_.level1.element.level2.element.level3.element.int, (int: Int) => int.toLong + 10) + assertTransformsFallibleConfigured(source, F.pure(expected))( + Field.fallibleComputedDeep(_.level1.element.level2.element.level3.element.int, (int: Int) => fallibleComputation(int + 10)) ) assertEachEquals( source .intoVia(DestToplevel1.apply) + .fallible .transform( - Field.computedDeep(_.level1.element.level2.element.level3.element.int, (int: Int) => int.toLong + 10) + Field.fallibleComputedDeep(_.level1.element.level2.element.level3.element.int, (int: Int) => fallibleComputation(int + 10)) ), Transformer .defineVia[SourceToplevel1](DestToplevel1.apply) + .fallible .build( - Field.computedDeep(_.level1.element.level2.element.level3.element.int, (int: Int) => int.toLong + 10) + Field.fallibleComputedDeep(_.level1.element.level2.element.level3.element.int, (int: Int) => fallibleComputation(int + 10)) ) .transform(source) - )(expected) + )(F.pure(expected)) } - test("Field.computedDeep works with collections") { - case class SourceToplevel1(level1: Vector[SourceLevel1]) - case class SourceLevel1(level2: Vector[SourceLevel2]) - case class SourceLevel2(level3: Vector[SourceLevel3]) - case class SourceLevel3(int: Int) - - case class DestToplevel1(level1: List[DestLevel1]) - case class DestLevel1(level2: List[DestLevel2]) - case class DestLevel2(level3: List[DestLevel3]) - case class DestLevel3(int: Long) - - val source = SourceToplevel1(Vector(SourceLevel1(Vector(SourceLevel2(Vector(SourceLevel3(1))))))) - val expected = DestToplevel1(List(DestLevel1(List(DestLevel2(List(DestLevel3(11))))))) - - assertTransformsConfigured(source, expected)( - Field.computedDeep(_.level1.element.level2.element.level3.element.int, (int: Int) => int.toLong + 10) - ) - - assertEachEquals( - source - .intoVia(DestToplevel1.apply) - .transform( - Field.computedDeep(_.level1.element.level2.element.level3.element.int, (int: Int) => int.toLong + 10) - ), - Transformer - .defineVia[SourceToplevel1](DestToplevel1.apply) - .build( - Field.computedDeep(_.level1.element.level2.element.level3.element.int, (int: Int) => int.toLong + 10) - ) - .transform(source) - )(expected) - } + // compiler crash - most likely https://github.com/arainko/ducktape/issues/128 + // test("Field.computedDeep works with collections") { + // case class SourceToplevel1(level1: Vector[SourceLevel1]) + // case class SourceLevel1(level2: Vector[SourceLevel2]) + // case class SourceLevel2(level3: Vector[SourceLevel3]) + // case class SourceLevel3(int: Int) + + // case class DestToplevel1(level1: List[DestLevel1]) + // case class DestLevel1(level2: List[DestLevel2]) + // case class DestLevel2(level3: List[DestLevel3]) + // case class DestLevel3(int: Positive) + + // val source = SourceToplevel1(Vector(SourceLevel1(Vector(SourceLevel2(Vector(SourceLevel3(1))))))) + // val expected = DestToplevel1(List(DestLevel1(List(DestLevel2(List(DestLevel3(Positive(11)))))))) + + // assertTransformsFallibleConfigured(source, F.pure(expected))( + // Field.fallibleComputedDeep(_.level1.element.level2.element.level3.element.int, (int: Int) => fallibleComputation(int + 10)) + // ) + + // assertEachEquals( + // source + // .intoVia(DestToplevel1.apply) + // .fallible + // .transform( + // Field.fallibleComputedDeep(_.level1.element.level2.element.level3.element.int, (int: Int) => fallibleComputation(int + 10)) + // ), + // Transformer + // .defineVia[SourceToplevel1](DestToplevel1.apply) + // .fallible + // .build( + // Field.fallibleComputedDeep(_.level1.element.level2.element.level3.element.int, (int: Int) => fallibleComputation(int + 10)) + // ) + // .transform(source) + // )(F.pure(expected)) + // } test("Field.computedDeep works with coproducts") { enum SourceToplevel1 { @@ -1020,24 +1029,24 @@ class AccumulatingNestedConfigurationSuite extends DucktapeSuite { } enum DestLevel3 { - case One(int: Long) + case One(int: Positive) case Two(str: String) } val source = SourceToplevel1.Level1(SourceLevel2.Level2(SourceLevel3.One(1))) - val expected = DestToplevel1.Level1(DestLevel2.Level2(DestLevel3.One(6))) + val expected = DestToplevel1.Level1(DestLevel2.Level2(DestLevel3.One(Positive(6)))) - assertTransformsConfigured(source, expected)( - Field.computedDeep( + assertTransformsFallibleConfigured(source, F.pure(expected))( + Field.fallibleComputedDeep( _.at[DestToplevel1.Level1].level2.at[DestLevel2.Level2].level3.at[DestLevel3.One], - (a: SourceLevel3.One) => DestLevel3.One(a.int.toLong + 5) + (a: SourceLevel3.One) => fallibleComputation(a.int + 5).map(pos => DestLevel3.One(pos)) ) ) - assertTransformsConfigured(source, expected)( - Field.computedDeep( + assertTransformsFallibleConfigured(source, F.pure(expected))( + Field.fallibleComputedDeep( _.at[DestToplevel1.Level1].level2.at[DestLevel2.Level2].level3.at[DestLevel3.One].int, - (a: Int) => a + 5L + (a: Int) => fallibleComputation(a + 5) ) ) } From 9f0a58acf158e40cdc63f6731920e19499face73 Mon Sep 17 00:00:00 2001 From: Aleksander Rainko Date: Wed, 30 Oct 2024 19:34:32 +0100 Subject: [PATCH 10/11] do not cast the expr to the exact subtype of a coproduct, use the supertype instead in FallibleInterpreter --- .../internal/FalliblePlanInterpreter.scala | 6 +- .../ducktape/internal/PlanConfigurer.scala | 2 +- ...AccumulatingNestedConfigurationSuite.scala | 69 +++++++++---------- 3 files changed, 38 insertions(+), 39 deletions(-) diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/FalliblePlanInterpreter.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/FalliblePlanInterpreter.scala index f7413fb3..9ebab0a0 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/FalliblePlanInterpreter.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/FalliblePlanInterpreter.scala @@ -103,12 +103,12 @@ private[ducktape] object FalliblePlanInterpreter { dest.tpe match { case '[destSupertype] => val branches = casePlans.map { plan => - (plan.source.tpe -> plan.dest.tpe) match { - case '[src] -> '[dest] => + plan.source.tpe match { + case '[src] => val sourceValue = '{ $value.asInstanceOf[src] } IfExpression.Branch( IsInstanceOf(value, plan.source.tpe), - recurse(plan, sourceValue, F).wrapped(F, Type.of[dest]) + recurse(plan, sourceValue, F).wrapped(F, Type.of[destSupertype]) ) } }.toList diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanConfigurer.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanConfigurer.scala index b738ce92..e04e660c 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanConfigurer.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanConfigurer.scala @@ -445,7 +445,7 @@ private[ducktape] object PlanConfigurer { (), ErrorMessage.InvalidConfigurationSourceType( config.sourceTpe.getOrElse(Type.of[Any]), - currentPlan.sourcePath.narrowedCurrentTpe, // TODO: Check if .currentTpe or rather sourcePath.last should be used here + currentPlan.sourcePath.narrowedCurrentTpe, instruction.side, instruction.span ) diff --git a/ducktape/src/test/scala/io/github/arainko/ducktape/fallible/accumulating/AccumulatingNestedConfigurationSuite.scala b/ducktape/src/test/scala/io/github/arainko/ducktape/fallible/accumulating/AccumulatingNestedConfigurationSuite.scala index aecef7cb..b74ce730 100644 --- a/ducktape/src/test/scala/io/github/arainko/ducktape/fallible/accumulating/AccumulatingNestedConfigurationSuite.scala +++ b/ducktape/src/test/scala/io/github/arainko/ducktape/fallible/accumulating/AccumulatingNestedConfigurationSuite.scala @@ -965,41 +965,40 @@ class AccumulatingNestedConfigurationSuite extends DucktapeSuite { } - // compiler crash - most likely https://github.com/arainko/ducktape/issues/128 - // test("Field.computedDeep works with collections") { - // case class SourceToplevel1(level1: Vector[SourceLevel1]) - // case class SourceLevel1(level2: Vector[SourceLevel2]) - // case class SourceLevel2(level3: Vector[SourceLevel3]) - // case class SourceLevel3(int: Int) - - // case class DestToplevel1(level1: List[DestLevel1]) - // case class DestLevel1(level2: List[DestLevel2]) - // case class DestLevel2(level3: List[DestLevel3]) - // case class DestLevel3(int: Positive) - - // val source = SourceToplevel1(Vector(SourceLevel1(Vector(SourceLevel2(Vector(SourceLevel3(1))))))) - // val expected = DestToplevel1(List(DestLevel1(List(DestLevel2(List(DestLevel3(Positive(11)))))))) - - // assertTransformsFallibleConfigured(source, F.pure(expected))( - // Field.fallibleComputedDeep(_.level1.element.level2.element.level3.element.int, (int: Int) => fallibleComputation(int + 10)) - // ) - - // assertEachEquals( - // source - // .intoVia(DestToplevel1.apply) - // .fallible - // .transform( - // Field.fallibleComputedDeep(_.level1.element.level2.element.level3.element.int, (int: Int) => fallibleComputation(int + 10)) - // ), - // Transformer - // .defineVia[SourceToplevel1](DestToplevel1.apply) - // .fallible - // .build( - // Field.fallibleComputedDeep(_.level1.element.level2.element.level3.element.int, (int: Int) => fallibleComputation(int + 10)) - // ) - // .transform(source) - // )(F.pure(expected)) - // } + test("Field.computedDeep works with collections") { + case class SourceToplevel1(level1: Vector[SourceLevel1]) + case class SourceLevel1(level2: Vector[SourceLevel2]) + case class SourceLevel2(level3: Vector[SourceLevel3]) + case class SourceLevel3(int: Int) + + case class DestToplevel1(level1: List[DestLevel1]) + case class DestLevel1(level2: List[DestLevel2]) + case class DestLevel2(level3: List[DestLevel3]) + case class DestLevel3(int: Positive) + + val source = SourceToplevel1(Vector(SourceLevel1(Vector(SourceLevel2(Vector(SourceLevel3(1))))))) + val expected = DestToplevel1(List(DestLevel1(List(DestLevel2(List(DestLevel3(Positive(11)))))))) + + assertTransformsFallibleConfigured(source, F.pure(expected))( + Field.fallibleComputedDeep(_.level1.element.level2.element.level3.element.int, (int: Int) => fallibleComputation(int + 10)) + ) + + assertEachEquals( + source + .intoVia(DestToplevel1.apply) + .fallible + .transform( + Field.fallibleComputedDeep(_.level1.element.level2.element.level3.element.int, (int: Int) => fallibleComputation(int + 10)) + ), + Transformer + .defineVia[SourceToplevel1](DestToplevel1.apply) + .fallible + .build( + Field.fallibleComputedDeep(_.level1.element.level2.element.level3.element.int, (int: Int) => fallibleComputation(int + 10)) + ) + .transform(source) + )(F.pure(expected)) + } test("Field.computedDeep works with coproducts") { enum SourceToplevel1 { From f2d3e1072bd4c90f42caa88ea67549426fecfc31 Mon Sep 17 00:00:00 2001 From: Aleksander Rainko Date: Wed, 30 Oct 2024 21:22:58 +0100 Subject: [PATCH 11/11] finish tests for Field.computedDeep and FIeld.fallibleComputedDeep, add docs --- .../configuring_fallible_transformations.md | 53 +++++ .../configuring_transformations.md | 48 +++- .../io/github/arainko/ducktape/Field.scala | 10 +- .../internal/ConfigInstructionRefiner.scala | 2 +- .../ducktape/internal/Configuration.scala | 2 +- .../ducktape/internal/ErrorMessage.scala | 6 +- .../internal/FalliblePlanInterpreter.scala | 2 +- .../ducktape/internal/PlanInterpreter.scala | 2 +- .../internal/TotalTransformations.scala | 4 +- .../ducktape/internal/extensions.scala | 2 - .../arainko/ducktape/DucktapeSuite.scala | 13 -- ...AccumulatingNestedConfigurationSuite.scala | 44 +++- .../FailFastNestedConfigurationSuite.scala | 206 ++++++++++++++++++ .../total/NestedConfigurationSuite.scala | 5 +- 14 files changed, 359 insertions(+), 40 deletions(-) diff --git a/docs/fallible_transformations/configuring_fallible_transformations.md b/docs/fallible_transformations/configuring_fallible_transformations.md index 4e5d09cf..83a45060 100644 --- a/docs/fallible_transformations/configuring_fallible_transformations.md +++ b/docs/fallible_transformations/configuring_fallible_transformations.md @@ -84,6 +84,7 @@ val good = wire.Person(name = "ValidName", age = 24, socialSecurityNo = "SOCIALN |:-----------------:|:-------------------:| | `Field.fallibleConst` | a fallible variant of `Field.const` that allows for supplying values wrapped in an `F` | | `Field.fallibleComputed` | a fallible variant of `Field.computed` that allows for supplying functions that return values wrapped in an `F` | +| `Field.fallibleComputedDeep` | a fallible variant of `Field.computedDeep` that allows for supplying functions that return values wrapped in an `F` | --- @@ -150,6 +151,58 @@ Docs.printCode( ``` @:@ +* `Field.fallibleComputedDeep` - a fallible variant of `Field.computedDeep` that allows for supplying functions that return values wrapped in an `F` + +```scala mdoc:nest:silent +given Mode.Accumulating.Either[String, List]() + +case class SourceToplevel1(level1: Option[SourceLevel1]) +case class SourceLevel1(level2: Option[SourceLevel2]) +case class SourceLevel2(int: Int) + +case class DestToplevel1(level1: Option[DestLevel1]) +case class DestLevel1(level2: Option[DestLevel2]) +case class DestLevel2(int: Positive) + +val source = SourceToplevel1(Some(SourceLevel1(Some(SourceLevel2(1))))) +``` + +@:select(underlying-code-13) +@:choice(visible) + +```scala mdoc +source + .into[DestToplevel1] + .fallible + .transform( + Field.fallibleComputedDeep( + _.level1.element.level2.element.int, + // the type here cannot be inferred automatically and needs to be provided by the user, + // a nice compiletime error message is shown (with a suggestion on what the proper type to use is) otherwise + (value: Int) => Positive.makeAccumulating(value + 10L)) + ) +``` + +@:choice(generated) +```scala mdoc:passthrough +import io.github.arainko.ducktape.docs.* + +Docs.printCode( + source + .into[DestToplevel1] + .fallible + .transform( + Field.fallibleComputedDeep( + _.level1.element.level2.element.int, + // the type here cannot be inferred automatically and needs to be provided by the user, + // a nice compiletime error message is shown (with a suggestion on what the proper type to use is) otherwise + (value: Int) => Positive.makeAccumulating(value + 10L)) + ) +) +``` + +@:@ + ### Coproduct configurations | **Name** | **Description** | diff --git a/docs/total_transformations/configuring_transformations.md b/docs/total_transformations/configuring_transformations.md index e8bd57c4..cead7760 100644 --- a/docs/total_transformations/configuring_transformations.md +++ b/docs/total_transformations/configuring_transformations.md @@ -195,8 +195,9 @@ What's worth noting is that any of the configuration options are purely a compil | **Name** | **Description** | |:-----------------:|:-------------------:| | `Field.const` | allows to supply a constant value for a given field | -| `Field.computed` | allows to compute a value with a function the shape of `Dest => FieldTpe` | +| `Field.computed` | allows to compute a value with a function that has a shape of `Dest => FieldTpe` | | `Field.default` | only works when a field's got a default value defined (defaults are not taken into consideration by default) | +| `Field.computedDeep` | allows to compute a deeply nested field (for example going through multiple `Options` or other collections) | | `Field.allMatching` | allow to supply a field source whose fields will replace all matching fields in the destination (given that the names and the types match up) | | `Field.fallbackToDefault` | falls back to default field values but ONLY in case a transformation cannot be created | | `Field.fallbackToNone` | falls back to `None` for `Option` fields for which a transformation cannot be created | @@ -279,9 +280,52 @@ Docs.printCode( ``` @:@ +* `Field.computedDeep` - allows to compute a deeply nested field (for example going through multiple `Options` or collections) + +```scala mdoc:nest:silent +case class SourceToplevel1(level1: Option[SourceLevel1]) +case class SourceLevel1(level2: Option[SourceLevel2]) +case class SourceLevel2(int: Int) + +case class DestToplevel1(level1: Option[DestLevel1]) +case class DestLevel1(level2: Option[DestLevel2]) +case class DestLevel2(int: Long) + +val source = SourceToplevel1(Some(SourceLevel1(Some(SourceLevel2(1))))) +``` + +@:select(underlying-code-13) +@:choice(visible) + +```scala mdoc +source + .into[DestToplevel1] + .transform( + Field.computedDeep( + _.level1.element.level2.element.int, + // the type here cannot be inferred automatically and needs to be provided by the user, + // a nice compiletime error message is shown (with a suggestion on what the proper type to use is) otherwise + (value: Int) => value + 10L + ) + ) +``` + +@:choice(generated) +```scala mdoc:passthrough +import io.github.arainko.ducktape.docs.* + +Docs.printCode( + source + .into[DestToplevel1] + .transform(Field.computedDeep(_.level1.element.level2.element.int, (value: Int) => value + 10L)) +) +``` + +@:@ + * `Field.allMatching` - allow to supply a field source whose fields will replace all matching fields in the destination (given that the names and the types match up) -```scala mdoc:silent +```scala mdoc:nest:silent case class FieldSource(color: String, digits: Long, extra: Int) val source = FieldSource("magenta", 123445678, 23) ``` diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/Field.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/Field.scala index 059cb30b..331112b9 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/Field.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/Field.scala @@ -22,6 +22,7 @@ object Field { function: A => F[DestFieldTpe] ): Field.Fallible[F, A, B] = ??? + @compileTimeOnly("Field.fallibleComputedDeep is only useable as a field configuration for transformations") def fallibleComputedDeep[F[+x], A, B, DestFieldTpe, SourceFieldTpe]( selector: Selector ?=> B => DestFieldTpe, function: SourceFieldTpe => F[DestFieldTpe] @@ -36,6 +37,7 @@ object Field { function: A => ComputedTpe ): Field[A, B] = ??? + @compileTimeOnly("Field.computedDeep is only useable as a field configuration for transformations") def computedDeep[A, B, DestFieldTpe, SourceFieldTpe, ComputedTpe]( selector: Selector ?=> B => DestFieldTpe, function: SourceFieldTpe => ComputedTpe @@ -63,12 +65,4 @@ object Field { @compileTimeOnly("Field.allMatching is only useable as a field configuration for transformations") def allMatching[A, B, ProductTpe](product: ProductTpe): Field[A, B] = ??? - - // inline def of[A]: Field.Of[A] = () - - // opaque type Of[A] = Unit - - // extension [A] (inline self: Of[A]) { - // inline def apply[B](f: A => B): A => B = f - // } } 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 5c41153f..5b83beec 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 @@ -9,7 +9,7 @@ private[ducktape] object ConfigInstructionRefiner { instruction match case inst @ Instruction.Static(_, _, config, _) => config match - case cfg: (Const | CaseComputed | FieldComputed | FieldComputedDeep | FieldReplacement) => inst.copy(config = cfg) + case cfg: (Const | CaseComputed | FieldComputed | FieldComputedDeep | FieldReplacement) => inst.copy(config = cfg) case fallible: (FallibleConst | FallibleFieldComputed | FallibleFieldComputedDeep | FallibleCaseComputed) => None case inst: (Instruction.Dynamic | Instruction.Bulk | Instruction.Regional | Instruction.Failed) => inst diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Configuration.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Configuration.scala index 8b52543c..f4cd0a4a 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Configuration.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Configuration.scala @@ -25,7 +25,7 @@ private[ducktape] enum Configuration[+F <: Fallible] { case FallibleFieldComputedDeep(destTpe: Type[?], override val sourceTpe: Type[?], function: Expr[Any => Any]) extends Configuration[Fallible] - + case FallibleCaseComputed(destTpe: Type[?], function: Expr[Any => Any]) extends Configuration[Fallible] } diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/ErrorMessage.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/ErrorMessage.scala index c552bda1..ececd1ca 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/ErrorMessage.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/ErrorMessage.scala @@ -43,7 +43,8 @@ private[ducktape] object ErrorMessage { val side = Side.Source } - final case class InvalidConfigurationDestType(configTpe: Type[?], expectedTpe: Type[?], side: Side, span: Span) extends ErrorMessage { + final case class InvalidConfigurationDestType(configTpe: Type[?], expectedTpe: Type[?], side: Side, span: Span) + extends ErrorMessage { def render(using Quotes): String = { val renderedConfigTpe = configTpe.repr.show @@ -52,7 +53,8 @@ private[ducktape] object ErrorMessage { } } - final case class InvalidConfigurationSourceType(configTpe: Type[?], expectedTpe: Type[?], side: Side, span: Span) extends ErrorMessage { + final case class InvalidConfigurationSourceType(configTpe: Type[?], expectedTpe: Type[?], side: Side, span: Span) + extends ErrorMessage { def render(using Quotes): String = { val renderedConfigTpe = configTpe.repr.show diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/FalliblePlanInterpreter.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/FalliblePlanInterpreter.scala index 9ebab0a0..91e65690 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/FalliblePlanInterpreter.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/FalliblePlanInterpreter.scala @@ -286,7 +286,7 @@ private[ducktape] object FalliblePlanInterpreter { case (fieldName, plan) -> index if source.fields.contains(fieldName) => val fieldValue = value.accessFieldByName(fieldName).asExpr recurse(plan, fieldValue, F).asFieldValue(index, plan.dest.tpe) - case (fieldName, plan) -> index => + case (fieldName, plan) -> index => recurse(plan, value, F).asFieldValue(index, plan.dest.tpe) } diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanInterpreter.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanInterpreter.scala index 03354dd8..17b78502 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanInterpreter.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/PlanInterpreter.scala @@ -160,7 +160,7 @@ private[ducktape] object PlanInterpreter { case Configuration.FieldComputed(_, function) => '{ $function.apply($toplevelValue) } case Configuration.FieldComputedDeep(tpe, sourceTpe, function) => - /*Expr.betaReduce(*/'{ $function.apply($value) }/*)*/ + '{ $function.apply($value) } case Configuration.FieldReplacement(source, name, tpe) => source.accessFieldByName(name).asExpr } diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/TotalTransformations.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/TotalTransformations.scala index 2230dbf7..ac256406 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/TotalTransformations.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/TotalTransformations.scala @@ -23,9 +23,7 @@ private[ducktape] object TotalTransformations { val plan = Planner.between(Structure.of[A](Path.empty(Type.of[A])), Structure.of[B](Path.empty(Type.of[B]))) val config = Configuration.parse(configs, ConfigParser.total) val totalPlan = Backend.refineOrReportErrorsAndAbort(plan, config) - val res = PlanInterpreter.run[A](totalPlan, value).asExprOf[B] - Logger.info(res.show) - res + PlanInterpreter.run[A](totalPlan, value).asExprOf[B] } inline def via[A, B, Func, Args <: FunctionArguments]( diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/extensions.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/extensions.scala index 4fcf9243..b85e278f 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/extensions.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/extensions.scala @@ -46,7 +46,5 @@ extension [A](self: A | None.type) { case a: A => caseA(a) // private[ducktape] inline def map[B](inline f: A => B): B | None.type = self.fold(None, f) - - } diff --git a/ducktape/src/test/scala/io/github/arainko/ducktape/DucktapeSuite.scala b/ducktape/src/test/scala/io/github/arainko/ducktape/DucktapeSuite.scala index 4bea382e..3c1649cc 100644 --- a/ducktape/src/test/scala/io/github/arainko/ducktape/DucktapeSuite.scala +++ b/ducktape/src/test/scala/io/github/arainko/ducktape/DucktapeSuite.scala @@ -31,19 +31,6 @@ trait DucktapeSuite extends FunSuite { Transformer.define[A, B].build().transform(source) )(expected) - // transparent inline def assertTransformsVia[A, B, Func, Args <: FunctionArguments]( - // source: A, - // inline func: A => B, - // inline func1: DefinitionViaBuilder.PartiallyApplied[A] => DefinitionViaBuilder[A, B, Func, Args], - // inline func2: A => AppliedViaBuilder[A, B, Func, Args], - // expected: B - // )(using Location) = - // assertEachEquals( - // source.via(func), - // func1(DefinitionViaBuilder.create[A]).build().transform(source), - // func2(source).transform() - // )(expected) - inline def assertTransformsFallible[F[+x], M <: Mode[F], A, B](using M)(source: A, expected: F[B])(using loc: Location) = assertEachEquals( source.fallibleTo[B], diff --git a/ducktape/src/test/scala/io/github/arainko/ducktape/fallible/accumulating/AccumulatingNestedConfigurationSuite.scala b/ducktape/src/test/scala/io/github/arainko/ducktape/fallible/accumulating/AccumulatingNestedConfigurationSuite.scala index b74ce730..e93bee7f 100644 --- a/ducktape/src/test/scala/io/github/arainko/ducktape/fallible/accumulating/AccumulatingNestedConfigurationSuite.scala +++ b/ducktape/src/test/scala/io/github/arainko/ducktape/fallible/accumulating/AccumulatingNestedConfigurationSuite.scala @@ -929,7 +929,7 @@ class AccumulatingNestedConfigurationSuite extends DucktapeSuite { )(F.pure(expected)) } - test("Field.computedDeep works with Options") { + test("Field.fallibleComputedDeep works with Options") { case class SourceToplevel1(level1: Option[SourceLevel1]) case class SourceLevel1(level2: Option[SourceLevel2]) case class SourceLevel2(level3: SourceLevel3) @@ -965,7 +965,7 @@ class AccumulatingNestedConfigurationSuite extends DucktapeSuite { } - test("Field.computedDeep works with collections") { + test("Field.fallibleComputedDeep works with collections") { case class SourceToplevel1(level1: Vector[SourceLevel1]) case class SourceLevel1(level2: Vector[SourceLevel2]) case class SourceLevel2(level3: Vector[SourceLevel3]) @@ -1000,7 +1000,7 @@ class AccumulatingNestedConfigurationSuite extends DucktapeSuite { )(F.pure(expected)) } - test("Field.computedDeep works with coproducts") { + test("Field.fallibleComputedDeep works with coproducts") { enum SourceToplevel1 { case Level1(level2: SourceLevel2) } @@ -1044,4 +1044,42 @@ class AccumulatingNestedConfigurationSuite extends DucktapeSuite { ) ) } + + test("Field.fallibleComputedDeep works with F-unwrapping") { + case class SourceToplevel1(level1: Vector[SourceLevel1]) + case class SourceLevel1(level2: Vector[SourceLevel2]) + case class SourceLevel2(level3: Vector[SourceLevel3]) + case class SourceLevel3(int: Either[List[String], Int]) + + case class DestToplevel1(level1: List[DestLevel1]) + case class DestLevel1(level2: List[DestLevel2]) + case class DestLevel2(level3: List[DestLevel3]) + case class DestLevel3(int: Int) + + val source = SourceToplevel1(Vector(SourceLevel1(Vector(SourceLevel2(Vector(SourceLevel3(Right(1)))))))) + val expected = DestToplevel1(List(DestLevel1(List(DestLevel2(List(DestLevel3(11))))))) + + assertTransformsFallibleConfigured(source, F.pure(expected))( + Field.fallibleComputedDeep( + _.level1.element.level2.element.level3.element.int, + (int: Either[List[String], Int]) => int.map(_ + 10) + ) + ) + + assertEachEquals( + source + .intoVia(DestToplevel1.apply) + .fallible + .transform( + Field.fallibleComputedDeep(_.level1.element.level2.element.level3.element.int, (int: Either[List[String], Int]) => int.map(_ + 10)) + ), + Transformer + .defineVia[SourceToplevel1](DestToplevel1.apply) + .fallible + .build( + Field.fallibleComputedDeep(_.level1.element.level2.element.level3.element.int, (int: Either[List[String], Int]) => int.map(_ + 10)) + ) + .transform(source) + )(F.pure(expected)) + } } diff --git a/ducktape/src/test/scala/io/github/arainko/ducktape/fallible/failfast/FailFastNestedConfigurationSuite.scala b/ducktape/src/test/scala/io/github/arainko/ducktape/fallible/failfast/FailFastNestedConfigurationSuite.scala index 10187c52..bf973cbb 100644 --- a/ducktape/src/test/scala/io/github/arainko/ducktape/fallible/failfast/FailFastNestedConfigurationSuite.scala +++ b/ducktape/src/test/scala/io/github/arainko/ducktape/fallible/failfast/FailFastNestedConfigurationSuite.scala @@ -876,4 +876,210 @@ class FailFastNestedConfigurationSuite extends DucktapeSuite { .transform(source) )(F.pure(expected)) } + + test("Field.fallibleComputedDeep works in deeply nested case classes") { + case class SourceToplevel1(level1: SourceLevel1) + case class SourceLevel1(level2: SourceLevel2) + case class SourceLevel2(int: Int) + + case class DestToplevel1(level1: DestLevel1) + case class DestLevel1(level2: DestLevel2) + case class DestLevel2(int: Positive) + + val source = SourceToplevel1(SourceLevel1(SourceLevel2(1))) + val expected = DestToplevel1(DestLevel1(DestLevel2(Positive(11)))) + + assertTransformsFallibleConfigured(source, F.pure(expected))( + Field.fallibleComputedDeep(_.level1.level2.int, (int: Int) => fallibleComputation(int + 10)) + ) + + assertEachEquals( + source + .intoVia(DestToplevel1.apply) + .fallible + .transform(Field.fallibleComputedDeep(_.level1.level2.int, (int: Int) => fallibleComputation(int + 10))), + Transformer + .defineVia[SourceToplevel1](DestToplevel1.apply) + .fallible + .build(Field.fallibleComputedDeep(_.level1.level2.int, (int: Int) => fallibleComputation(int + 10))) + .transform(source) + )(F.pure(expected)) + + } + + test("Field.fallibleComputedDeep works with deeply nested tuples") { + val source = Tuple1(Tuple1(Tuple1(1))) + + val expected = Tuple1(Tuple1(Tuple1(Positive(11)))) + + assertTransformsFallibleConfigured(source, F.pure(expected))( + Field.fallibleComputedDeep(_.apply(0).apply(0).apply(0), (int: Int) => fallibleComputation(int + 10)) + ) + + assertEachEquals( + source + .intoVia(Tuple1.apply[Tuple1[Tuple1[Positive]]]) + .fallible + .transform(Field.fallibleComputedDeep(_._1._1._1, (int: Int) => fallibleComputation(int + 10))), + Transformer + .defineVia[Tuple1[Tuple1[Tuple1[Int]]]](Tuple1.apply[Tuple1[Tuple1[Positive]]]) + .fallible + .build(Field.fallibleComputedDeep(_._1._1._1, (int: Int) => fallibleComputation(int + 10))) + .transform(source) + )(F.pure(expected)) + } + + test("Field.fallibleComputedDeep works with Options") { + case class SourceToplevel1(level1: Option[SourceLevel1]) + case class SourceLevel1(level2: Option[SourceLevel2]) + case class SourceLevel2(level3: SourceLevel3) + case class SourceLevel3(int: Int) + + case class DestToplevel1(level1: Option[DestLevel1]) + case class DestLevel1(level2: Option[DestLevel2]) + case class DestLevel2(level3: Option[DestLevel3]) + case class DestLevel3(int: Positive) + + val source = SourceToplevel1(Some(SourceLevel1(Some(SourceLevel2(SourceLevel3(1)))))) + val expected = DestToplevel1(Some(DestLevel1(Some(DestLevel2(Some(DestLevel3(Positive(11)))))))) + + assertTransformsFallibleConfigured(source, F.pure(expected))( + Field.fallibleComputedDeep(_.level1.element.level2.element.level3.element.int, (int: Int) => fallibleComputation(int + 10)) + ) + + assertEachEquals( + source + .intoVia(DestToplevel1.apply) + .fallible + .transform( + Field.fallibleComputedDeep(_.level1.element.level2.element.level3.element.int, (int: Int) => fallibleComputation(int + 10)) + ), + Transformer + .defineVia[SourceToplevel1](DestToplevel1.apply) + .fallible + .build( + Field.fallibleComputedDeep(_.level1.element.level2.element.level3.element.int, (int: Int) => fallibleComputation(int + 10)) + ) + .transform(source) + )(F.pure(expected)) + + } + + test("Field.fallibleComputedDeep works with collections") { + case class SourceToplevel1(level1: Vector[SourceLevel1]) + case class SourceLevel1(level2: Vector[SourceLevel2]) + case class SourceLevel2(level3: Vector[SourceLevel3]) + case class SourceLevel3(int: Int) + + case class DestToplevel1(level1: List[DestLevel1]) + case class DestLevel1(level2: List[DestLevel2]) + case class DestLevel2(level3: List[DestLevel3]) + case class DestLevel3(int: Positive) + + val source = SourceToplevel1(Vector(SourceLevel1(Vector(SourceLevel2(Vector(SourceLevel3(1))))))) + val expected = DestToplevel1(List(DestLevel1(List(DestLevel2(List(DestLevel3(Positive(11)))))))) + + assertTransformsFallibleConfigured(source, F.pure(expected))( + Field.fallibleComputedDeep(_.level1.element.level2.element.level3.element.int, (int: Int) => fallibleComputation(int + 10)) + ) + + assertEachEquals( + source + .intoVia(DestToplevel1.apply) + .fallible + .transform( + Field.fallibleComputedDeep(_.level1.element.level2.element.level3.element.int, (int: Int) => fallibleComputation(int + 10)) + ), + Transformer + .defineVia[SourceToplevel1](DestToplevel1.apply) + .fallible + .build( + Field.fallibleComputedDeep(_.level1.element.level2.element.level3.element.int, (int: Int) => fallibleComputation(int + 10)) + ) + .transform(source) + )(F.pure(expected)) + } + + test("Field.fallibleComputedDeep works with coproducts") { + enum SourceToplevel1 { + case Level1(level2: SourceLevel2) + } + + enum SourceLevel2 { + case Level2(level3: SourceLevel3) + } + + enum SourceLevel3 { + case One(int: Int) + case Two(str: String) + } + + enum DestToplevel1 { + case Level1(level2: DestLevel2) + } + + enum DestLevel2 { + case Level2(level3: DestLevel3) + } + + enum DestLevel3 { + case One(int: Positive) + case Two(str: String) + } + + val source = SourceToplevel1.Level1(SourceLevel2.Level2(SourceLevel3.One(1))) + val expected = DestToplevel1.Level1(DestLevel2.Level2(DestLevel3.One(Positive(6)))) + + assertTransformsFallibleConfigured(source, F.pure(expected))( + Field.fallibleComputedDeep( + _.at[DestToplevel1.Level1].level2.at[DestLevel2.Level2].level3.at[DestLevel3.One], + (a: SourceLevel3.One) => fallibleComputation(a.int + 5).map(pos => DestLevel3.One(pos)) + ) + ) + + assertTransformsFallibleConfigured(source, F.pure(expected))( + Field.fallibleComputedDeep( + _.at[DestToplevel1.Level1].level2.at[DestLevel2.Level2].level3.at[DestLevel3.One].int, + (a: Int) => fallibleComputation(a + 5) + ) + ) + } + + test("Field.fallibleComputedDeep works with F-unwrapping") { + case class SourceToplevel1(level1: Vector[SourceLevel1]) + case class SourceLevel1(level2: Vector[SourceLevel2]) + case class SourceLevel2(level3: Vector[SourceLevel3]) + case class SourceLevel3(int: Either[List[String], Int]) + + case class DestToplevel1(level1: List[DestLevel1]) + case class DestLevel1(level2: List[DestLevel2]) + case class DestLevel2(level3: List[DestLevel3]) + case class DestLevel3(int: Int) + + val source = SourceToplevel1(Vector(SourceLevel1(Vector(SourceLevel2(Vector(SourceLevel3(Right(1)))))))) + val expected = DestToplevel1(List(DestLevel1(List(DestLevel2(List(DestLevel3(11))))))) + + assertTransformsFallibleConfigured(source, F.pure(expected))( + Field.fallibleComputedDeep( + _.level1.element.level2.element.level3.element.int, + (int: Either[List[String], Int]) => int.map(_ + 10) + ) + ) + + assertEachEquals( + source + .intoVia(DestToplevel1.apply) + .fallible + .transform( + Field.fallibleComputedDeep(_.level1.element.level2.element.level3.element.int, (int: Either[List[String], Int]) => int.map(_ + 10)) + ), + Transformer + .defineVia[SourceToplevel1](DestToplevel1.apply) + .fallible + .build( + Field.fallibleComputedDeep(_.level1.element.level2.element.level3.element.int, (int: Either[List[String], Int]) => int.map(_ + 10)) + ) + .transform(source) + )(F.pure(expected)) + } } diff --git a/ducktape/src/test/scala/io/github/arainko/ducktape/total/NestedConfigurationSuite.scala b/ducktape/src/test/scala/io/github/arainko/ducktape/total/NestedConfigurationSuite.scala index 46642e76..894fd8ae 100644 --- a/ducktape/src/test/scala/io/github/arainko/ducktape/total/NestedConfigurationSuite.scala +++ b/ducktape/src/test/scala/io/github/arainko/ducktape/total/NestedConfigurationSuite.scala @@ -977,8 +977,7 @@ class NestedConfigurationSuite extends DucktapeSuite { ) }: @nowarn - // TODO: Think this through, should this error out? This currently picks up the 'nearest' source field to the dest one - test("Field.computedDeep works correctly when a field on the same level is missing in the Source".ignore) { + test("Field.computedDeep works correctly when a field on the same level is missing in the Source") { case class SourceToplevel1(level1: SourceLevel1) case class SourceLevel1(level2: SourceLevel2) case class SourceLevel2(int: Int) @@ -991,7 +990,7 @@ class NestedConfigurationSuite extends DucktapeSuite { val expected = DestToplevel1(DestLevel1(DestLevel2(1, "1CONF"))) assertTransformsConfigured(source, expected)( - Field.computedDeep(_.level1.level2.extra, (a: SourceLevel2) => a.int.toString()) + Field.computedDeep(_.level1.level2.extra, (a: SourceLevel2) => a.int.toString() + "CONF") ) }