diff --git a/ducktapeNext/src/main/scala/io/github/arainko/ducktape/AppliedBuilder.scala b/ducktapeNext/src/main/scala/io/github/arainko/ducktape/AppliedBuilder.scala index 710876cb..ce5bbc7f 100644 --- a/ducktapeNext/src/main/scala/io/github/arainko/ducktape/AppliedBuilder.scala +++ b/ducktapeNext/src/main/scala/io/github/arainko/ducktape/AppliedBuilder.scala @@ -1,8 +1,8 @@ package io.github.arainko.ducktape -import io.github.arainko.ducktape.internal.{Transformations, TransformationSite} +import io.github.arainko.ducktape.internal.{Transformations} final class AppliedBuilder[Source, Dest](value: Source) { inline def transform(inline config: Field[Source, Dest] | Case[Source, Dest]*): Dest = - Transformations.between[Source, Dest](value, TransformationSite.Transformation, config*) + Transformations.between[Source, Dest](value, "transformation", config*) } diff --git a/ducktapeNext/src/main/scala/io/github/arainko/ducktape/AppliedViaBuilder.scala b/ducktapeNext/src/main/scala/io/github/arainko/ducktape/AppliedViaBuilder.scala index 1e6d24d8..6b36f647 100644 --- a/ducktapeNext/src/main/scala/io/github/arainko/ducktape/AppliedViaBuilder.scala +++ b/ducktapeNext/src/main/scala/io/github/arainko/ducktape/AppliedViaBuilder.scala @@ -1,10 +1,10 @@ package io.github.arainko.ducktape -import io.github.arainko.ducktape.internal.{Transformations, TransformationSite} +import io.github.arainko.ducktape.internal.Transformations final class AppliedViaBuilder[Source, Dest, Func, Args <: FunctionArguments] private (value: Source, function: Func) { inline def transform(inline config: Field[Source, Args] | Case[Source, Args]*): Dest = - Transformations.via[Source, Dest, Func, Args](value, function, TransformationSite.Transformation, config*) + Transformations.via[Source, Dest, Func, Args](value, function, "transformation", config*) } object AppliedViaBuilder { diff --git a/ducktapeNext/src/main/scala/io/github/arainko/ducktape/DefinitionBuilder.scala b/ducktapeNext/src/main/scala/io/github/arainko/ducktape/DefinitionBuilder.scala index e2509cda..73200b89 100644 --- a/ducktapeNext/src/main/scala/io/github/arainko/ducktape/DefinitionBuilder.scala +++ b/ducktapeNext/src/main/scala/io/github/arainko/ducktape/DefinitionBuilder.scala @@ -1,8 +1,8 @@ package io.github.arainko.ducktape -import io.github.arainko.ducktape.internal.{Transformations, TransformationSite} +import io.github.arainko.ducktape.internal.Transformations final class DefinitionBuilder[Source, Dest] { inline def build(inline config: Field[Source, Dest] | Case[Source, Dest]*): Transformer[Source, Dest] = source => - Transformations.between[Source, Dest](source, TransformationSite.Definition, config*) + Transformations.between[Source, Dest](source, "definition", config*) } diff --git a/ducktapeNext/src/main/scala/io/github/arainko/ducktape/DefinitionViaBuilder.scala b/ducktapeNext/src/main/scala/io/github/arainko/ducktape/DefinitionViaBuilder.scala index 0635861c..1119accc 100644 --- a/ducktapeNext/src/main/scala/io/github/arainko/ducktape/DefinitionViaBuilder.scala +++ b/ducktapeNext/src/main/scala/io/github/arainko/ducktape/DefinitionViaBuilder.scala @@ -1,12 +1,12 @@ package io.github.arainko.ducktape -import io.github.arainko.ducktape.internal.{ TransformationSite, Transformations } +import io.github.arainko.ducktape.internal.Transformations final class DefinitionViaBuilder[Source, Dest, Func, Args <: FunctionArguments] private (function: Func) { transparent inline def build(inline config: Field[Source, Args] | Case[Source, Args]*): Transformer[Source, Dest] = new Transformer[Source, Dest] { def transform(value: Source): Dest = - Transformations.via[Source, Dest, Func, Args](value, function, TransformationSite.Definition, config*) + Transformations.via[Source, Dest, Func, Args](value, function, "definition", config*) } } diff --git a/ducktapeNext/src/main/scala/io/github/arainko/ducktape/Transformer.scala b/ducktapeNext/src/main/scala/io/github/arainko/ducktape/Transformer.scala index ecf50bf0..3fdad8f7 100644 --- a/ducktapeNext/src/main/scala/io/github/arainko/ducktape/Transformer.scala +++ b/ducktapeNext/src/main/scala/io/github/arainko/ducktape/Transformer.scala @@ -1,13 +1,13 @@ package io.github.arainko.ducktape import io.github.arainko.ducktape.DefinitionViaBuilder.PartiallyApplied -import io.github.arainko.ducktape.internal.{Transformations, TransformationSite} +import io.github.arainko.ducktape.internal.Transformations trait Transformer[Source, Dest] extends Transformer.Derived[Source, Dest] object Transformer { inline given derive[Source, Dest]: Transformer.Derived[Source, Dest] = new { - def transform(value: Source): Dest = Transformations.between[Source, Dest](value, TransformationSite.Definition) + def transform(value: Source): Dest = Transformations.between[Source, Dest](value, "definition") } def define[Source, Dest]: DefinitionBuilder[Source, Dest] = diff --git a/ducktapeNext/src/main/scala/io/github/arainko/ducktape/internal/TransformationSite.scala b/ducktapeNext/src/main/scala/io/github/arainko/ducktape/internal/TransformationSite.scala index eaf53d2e..b4572a00 100644 --- a/ducktapeNext/src/main/scala/io/github/arainko/ducktape/internal/TransformationSite.scala +++ b/ducktapeNext/src/main/scala/io/github/arainko/ducktape/internal/TransformationSite.scala @@ -1,22 +1,19 @@ package io.github.arainko.ducktape.internal -import scala.quoted.FromExpr -import scala.quoted.Expr -import scala.quoted.Quotes +import scala.quoted.* -enum TransformationSite { +private[ducktape] enum TransformationSite { case Definition case Transformation } -object TransformationSite { - given fromExpr: FromExpr[TransformationSite] = - new { - def unapply(x: Expr[TransformationSite])(using Quotes): Option[TransformationSite] = - x match { - case '{ TransformationSite.Definition } => Some(TransformationSite.Definition) - case '{ TransformationSite.Transformation } => Some(TransformationSite.Transformation) - case _ => None - } - } +private[ducktape] object TransformationSite { + def fromStringExpr(value: Expr["transformation" | "definition"])(using Quotes): TransformationSite = { + import quotes.reflect.* + + summon[FromExpr["transformation" | "definition"]].unapply(value).map { + case "transformation" => TransformationSite.Transformation + case "definition" => TransformationSite.Definition + }.getOrElse(report.errorAndAbort("Couldn't parse TransformationSite from a literal string", value)) + } } diff --git a/ducktapeNext/src/main/scala/io/github/arainko/ducktape/internal/Transformations.scala b/ducktapeNext/src/main/scala/io/github/arainko/ducktape/internal/Transformations.scala index f4590a57..b1f27c31 100644 --- a/ducktapeNext/src/main/scala/io/github/arainko/ducktape/internal/Transformations.scala +++ b/ducktapeNext/src/main/scala/io/github/arainko/ducktape/internal/Transformations.scala @@ -9,16 +9,16 @@ import scala.quoted.runtime.StopMacroExpansion private[ducktape] object Transformations { inline def between[A, B]( value: A, - inline transformationSite: TransformationSite, + inline transformationSite: "transformation" | "definition", inline configs: Field[A, B] | Case[A, B]* ): B = ${ createTransformationBetween[A, B]('value, 'transformationSite, 'configs) } private def createTransformationBetween[A: Type, B: Type]( value: Expr[A], - transformationSite: Expr[TransformationSite], + transformationSite: Expr["transformation" | "definition"], configs: Expr[Seq[Field[A, B] | Case[A, B]]] )(using Quotes): Expr[B] = { - given TransformationSite = transformationSite.valueOrAbort + given TransformationSite = TransformationSite.fromStringExpr(transformationSite) val plan = Planner.between(Structure.of[A], Structure.of[B]) val config = Configuration.parse(configs) createTransformation(value, plan, config).asExprOf[B] @@ -27,13 +27,13 @@ private[ducktape] object Transformations { inline def via[A, B, Func, Args <: FunctionArguments]( value: A, function: Func, - inline transformationSite: TransformationSite, + inline transformationSite: "transformation" | "definition", inline configs: Field[A, Args] | Case[A, Args]* ): B = ${ createTransformationVia[A, B, Func, Args]('value, 'function, 'transformationSite, 'configs) } transparent inline def viaInferred[A, Func, Args <: FunctionArguments]( value: A, - inline transformationSite: TransformationSite, + inline transformationSite: "transformation" | "definition", inline function: Func, inline configs: Field[A, Args] | Case[A, Args]* ): Any = ${ createTransformationViaInferred('value, 'function, 'transformationSite, 'configs) } @@ -41,10 +41,10 @@ private[ducktape] object Transformations { private def createTransformationViaInferred[A: Type, Func: Type, Args <: FunctionArguments: Type]( value: Expr[A], function: Expr[Func], - transformationSite: Expr[TransformationSite], + transformationSite: Expr["transformation" | "definition"], configs: Expr[Seq[Field[A, Args] | Case[A, Args]]] )(using Quotes) = { - given TransformationSite = transformationSite.valueOrAbort + given TransformationSite = TransformationSite.fromStringExpr(transformationSite) val plan = Function @@ -68,10 +68,10 @@ private[ducktape] object Transformations { private def createTransformationVia[A: Type, B: Type, Func: Type, Args <: FunctionArguments: Type]( value: Expr[A], function: Expr[Func], - transformationSite: Expr[TransformationSite], + transformationSite: Expr["transformation" | "definition"], configs: Expr[Seq[Field[A, Args] | Case[A, Args]]] )(using Quotes) = { - given TransformationSite = transformationSite.valueOrAbort + given TransformationSite = TransformationSite.fromStringExpr(transformationSite) val plan = Function diff --git a/ducktapeNext/src/main/scala/io/github/arainko/ducktape/syntax.scala b/ducktapeNext/src/main/scala/io/github/arainko/ducktape/syntax.scala index 7a014f66..6f9e447a 100644 --- a/ducktapeNext/src/main/scala/io/github/arainko/ducktape/syntax.scala +++ b/ducktapeNext/src/main/scala/io/github/arainko/ducktape/syntax.scala @@ -1,14 +1,14 @@ package io.github.arainko.ducktape -import io.github.arainko.ducktape.internal.{Transformations, TransformationSite} +import io.github.arainko.ducktape.internal.Transformations extension [Source](source: Source) { - inline def to[Dest]: Dest = Transformations.between[Source, Dest](source, TransformationSite.Transformation) + inline def to[Dest]: Dest = Transformations.between[Source, Dest](source, "transformation") def into[Dest]: AppliedBuilder[Source, Dest] = AppliedBuilder[Source, Dest](source) transparent inline def via[Func](inline function: Func): Any = - Transformations.viaInferred[Source, Func, Nothing](source, TransformationSite.Transformation, function) + Transformations.viaInferred[Source, Func, Nothing](source, "transformation", function) transparent inline def intoVia[Func](inline function: Func): Any = AppliedViaBuilder.create(source, function) diff --git a/ducktapeNext/src/test/scala/io/github/arainko/ducktape/total/NestedConfigurationSuite.scala b/ducktapeNext/src/test/scala/io/github/arainko/ducktape/total/NestedConfigurationSuite.scala index 41995a57..e3c8e0d8 100644 --- a/ducktapeNext/src/test/scala/io/github/arainko/ducktape/total/NestedConfigurationSuite.scala +++ b/ducktapeNext/src/test/scala/io/github/arainko/ducktape/total/NestedConfigurationSuite.scala @@ -1,6 +1,7 @@ package io.github.arainko.ducktape.total import io.github.arainko.ducktape.* +import io.github.arainko.ducktape.internal.* import scala.annotation.nowarn @@ -409,4 +410,57 @@ class NestedConfigurationSuite extends DucktapeSuite { "Configuration is not valid since the provided type (123) is not a subtype of DestLevel3 @ SourceToplevel1.at[SourceToplevel1.Level1].level2.at[SourceLevel2.Level2].level3.at[SourceLevel3.Extra]" ) }: @nowarn("msg=unused local definition") + + test("Case.computed works for nested cases") { + enum SourceToplevel1 { + case Level1(level2: SourceLevel2) + } + + enum SourceLevel2 { + case Level2(level3: SourceLevel3) + } + + enum SourceLevel3 { + case One(int: Int) + case Two(str: String) + case Extra(int: Int) + } + + enum DestToplevel1 { + case Level1(level2: DestLevel2) + } + + enum DestLevel2 { + case Level2(level3: DestLevel3) + } + + enum DestLevel3 { + case One(int: Int) + case Two(str: String) + } + + val source = SourceToplevel1.Level1(SourceLevel2.Level2(SourceLevel3.Extra(1))) + val expected = DestToplevel1.Level1(DestLevel2.Level2(DestLevel3.One(6))) + + 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) + + } }