diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Context.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Context.scala index 7e9ae2ef..b3b385c5 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Context.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Context.scala @@ -1,20 +1,42 @@ package io.github.arainko.ducktape.internal +import io.github.arainko.ducktape.internal.Context.NamedTuples + +import scala.quoted.* + private[ducktape] sealed trait Context { type F <: Fallible def summoner: Summoner[F] def transformationSite: TransformationSite + def namedTuples: Option[NamedTuples] final def reifyPlan[FF <: Fallible](value: Plan[Erroneous, F])(using ev: Plan[Erroneous, F] =:= Plan[Erroneous, FF]) = ev(value) - final def toTotal: Context.Total = Context.Total(transformationSite) + final def toTotal: Context.Total = Context.Total(transformationSite, namedTuples) } private[ducktape] object Context { type Of[F0 <: Fallible] = Context { type F = F0 } + // unappliedNamedTuple is the type lambda [Names, Values] =>> NamedTuple[Names, Values], used to harvest its type symbol later on + final class NamedTuples private (private val unappliedNamedTuple: Type[?]) { + def isNamedTuple(tpe: Type[?])(using Quotes) = + tpe.repr.dealias.typeSymbol == unappliedNamedTuple.repr.typeSymbol + } + + object NamedTuples { + def create(using Quotes): Option[NamedTuples] = { + import quotes.reflect.* + Symbol + .requiredModule("scala.NamedTuple") + .declaredType("NamedTuple") + .headOption + .map(sym => NamedTuples(sym.typeRef.asType)) + } + } + transparent inline def current(using ctx: Context): ctx.type = ctx extension [F <: Fallible](self: Context.Of[F]) { @@ -25,13 +47,15 @@ private[ducktape] object Context { wrapperType: WrapperType[G], transformationSite: TransformationSite, summoner: Summoner.PossiblyFallible[G], - mode: TransformationMode[G] + mode: TransformationMode[G], + namedTuples: Option[NamedTuples] ) extends Context { final type F = Fallible } case class Total( - transformationSite: TransformationSite + transformationSite: TransformationSite, + namedTuples: Option[NamedTuples] ) extends Context { final type F = Nothing diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/FallibleTransformations.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/FallibleTransformations.scala index c515e2ad..78deca66 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/FallibleTransformations.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/FallibleTransformations.scala @@ -22,7 +22,8 @@ private[ducktape] object FallibleTransformations { WrapperType.create[F], TransformationSite.fromStringExpr(transformationSite), Summoner.PossiblyFallible[F], - TransformationMode.create(F) + TransformationMode.create(F), + Context.NamedTuples.create ) val sourceStruct = Structure.of[A](Path.empty(Type.of[A])) @@ -53,7 +54,8 @@ private[ducktape] object FallibleTransformations { WrapperType.create[F], TransformationSite.fromStringExpr(transformationSite), Summoner.PossiblyFallible[F], - TransformationMode.create(F) + TransformationMode.create(F), + Context.NamedTuples.create ) val sourceStruct = Structure.of[A](Path.empty(Type.of[A])) @@ -95,7 +97,8 @@ private[ducktape] object FallibleTransformations { WrapperType.create[F], TransformationSite.Transformation, Summoner.PossiblyFallible[F], - TransformationMode.create(F) + TransformationMode.create(F), + Context.NamedTuples.create ) val sourceStruct = Structure.of[A](Path.empty(Type.of[A])) diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Planner.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Planner.scala index 8696b2d0..9b1d218e 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Planner.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Planner.scala @@ -446,7 +446,7 @@ private[ducktape] object Planner { )(using Quotes, Depth, Context.Of[F], PlanFlags, Flag.Linter): Option[Plan[Erroneous, F]] = PartialFunction.condOpt(Context.current *: structs) { case ( - ctx @ Context.PossiblyFallible(_, _, _, mode: TransformationMode.FailFast[f]), + ctx @ Context.PossiblyFallible(_, _, _, mode: TransformationMode.FailFast[f], _), source @ Wrapped(tpe, _, path, underlying), dest ) => @@ -464,7 +464,7 @@ private[ducktape] object Planner { } case ( - ctx @ Context.PossiblyFallible(_, _, _, TransformationMode.Accumulating(mode, Some(localMode))), + ctx @ Context.PossiblyFallible(_, _, _, TransformationMode.Accumulating(mode, Some(localMode)), _), source @ Wrapped(tpe, _, path, underlying), dest ) => @@ -482,7 +482,7 @@ private[ducktape] object Planner { } case ( - ctx @ Context.PossiblyFallible(_, _, _, TransformationMode.Accumulating(mode, None)), + ctx @ Context.PossiblyFallible(_, _, _, TransformationMode.Accumulating(mode, None), _), source @ Wrapped(tpe, _, path, underlying), dest ) => diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Structure.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Structure.scala index 742b072b..e3f857d2 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Structure.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Structure.scala @@ -209,7 +209,7 @@ private[ducktape] object Structure { .to(VectorMap) val kind = - if Type.of[A].repr.dealias.typeSymbol.fullName == "scala.NamedTuple$.NamedTuple" then { + if Type.of[A].isNamedTuple then { val normalizedErasedTupleTpe = Tuples.rollup(typeElems.toVector) Structure.Product.Kind.NamedTuple(normalizedErasedTupleTpe) } else Structure.Product.Kind.CaseClass 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 781a99e1..cb39ac11 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 @@ -17,7 +17,8 @@ private[ducktape] object TotalTransformations { configs: Expr[Seq[Field[A, B] | Case[A, B]]] )(using Quotes): Expr[B] = { given Context.Total( - TransformationSite.fromStringExpr(transformationSite) + TransformationSite.fromStringExpr(transformationSite), + Context.NamedTuples.create ) val (config, flags) = Configuration.parse(configs, ConfigParser.total) @@ -43,7 +44,8 @@ private[ducktape] object TotalTransformations { )(using Quotes) = { given Context.Total( - TransformationSite.Transformation + TransformationSite.Transformation, + Context.NamedTuples.create ) val sourceStruct = Structure.of[A](Path.empty(Type.of[A])) @@ -72,7 +74,8 @@ private[ducktape] object TotalTransformations { configs: Expr[Seq[Field[A, Args] | Case[A, Args]]] )(using Quotes) = { given Context.Total( - TransformationSite.fromStringExpr(transformationSite) + TransformationSite.fromStringExpr(transformationSite), + Context.NamedTuples.create ) val sourceStruct = Structure.of[A](Path.empty(Type.of[A])) diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Tuples.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Tuples.scala index b90cd8b7..6039b134 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Tuples.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/Tuples.scala @@ -4,6 +4,7 @@ import scala.annotation.tailrec import scala.quoted.* private[ducktape] object Tuples { + def unroll(tpe: Type[?])(using Quotes): List[quotes.reflect.TypeRepr] = { @tailrec def loop(using Quotes)(curr: Type[?], acc: List[quotes.reflect.TypeRepr]): List[quotes.reflect.TypeRepr] = { import quotes.reflect.* diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/WrapperType.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/WrapperType.scala index 28fc1868..5fb0c78b 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/WrapperType.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/WrapperType.scala @@ -34,7 +34,7 @@ private[ducktape] object WrapperType { def unapply(using Quotes, Context)(tpe: Type[?]) = Context.current match { case ctx: Context.PossiblyFallible[?] => ctx.wrapperType.unapply(tpe) - case Context.Total(_) => None + case Context.Total(_, _) => None } case object Optional extends WrapperType[Option] { 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 28005e76..36bc8fab 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 @@ -13,6 +13,13 @@ extension (tpe: Type[? <: AnyKind]) { private[ducktape] def repr(using Quotes): quotes.reflect.TypeRepr = quotes.reflect.TypeRepr.of(using tpe) + + private[ducktape] def isNamedTuple(using Context, Quotes): Boolean = { + Context.current.namedTuples.match { + case None => false + case Some(namedTuples) => namedTuples.isNamedTuple(tpe) + } + } } extension (expr: Expr[Any]) {