Skip to content

Commit

Permalink
introduce transformation site
Browse files Browse the repository at this point in the history
  • Loading branch information
arainko committed Nov 21, 2023
1 parent a5baecd commit 21d783f
Show file tree
Hide file tree
Showing 14 changed files with 108 additions and 34 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package io.github.arainko.ducktape

import io.github.arainko.ducktape.internal.Transformations
import io.github.arainko.ducktape.internal.{Transformations, TransformationSite}

final class AppliedBuilder[Source, Dest](value: Source) {
inline def transform(inline config: Field[Source, Dest] | Case[Source, Dest]*): Dest =
Transformations.between[Source, Dest](value, config*)
Transformations.between[Source, Dest](value, TransformationSite.Transformation, config*)
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package io.github.arainko.ducktape

import io.github.arainko.ducktape.internal.Transformations
import io.github.arainko.ducktape.internal.{Transformations, TransformationSite}

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, config*)
Transformations.via[Source, Dest, Func, Args](value, function, TransformationSite.Transformation, config*)
}

object AppliedViaBuilder {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package io.github.arainko.ducktape

import io.github.arainko.ducktape.internal.Transformations
import io.github.arainko.ducktape.internal.{Transformations, TransformationSite}

class DefinitionBuilder[Source, Dest] {
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, config*)
Transformations.between[Source, Dest](source, TransformationSite.Definition, config*)
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package io.github.arainko.ducktape

import io.github.arainko.ducktape.internal.Transformations
import io.github.arainko.ducktape.internal.{ TransformationSite, 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, config*)
def transform(value: Source): Dest =
Transformations.via[Source, Dest, Func, Args](value, function, TransformationSite.Definition, config*)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package io.github.arainko.ducktape

import io.github.arainko.ducktape.DefinitionViaBuilder.PartiallyApplied
import io.github.arainko.ducktape.internal.Transformations
import io.github.arainko.ducktape.internal.{Transformations, TransformationSite}

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)
def transform(value: Source): Dest = Transformations.between[Source, Dest](value, TransformationSite.Definition)
}

def define[Source, Dest]: DefinitionBuilder[Source, Dest] =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ private[ducktape] enum Configuration derives Debug {
def tpe: Type[?]

case Const(value: Expr[Any], tpe: Type[?])
case Computed(tpe: Type[?], function: Expr[Any => Any])
case CaseComputed(tpe: Type[?], function: Expr[Any => Any])
case FieldComputed(tpe: Type[?], function: Expr[Any => Any])
case FieldReplacement(source: Expr[Any], name: String, tpe: Type[?])
}

Expand Down Expand Up @@ -86,7 +87,7 @@ private[ducktape] object Configuration {
Configuration.At.Successful(
path,
Target.Dest,
Configuration.Computed(computedTpe.tpe.asType, function.asExpr.asInstanceOf[Expr[Any => Any]]),
Configuration.FieldComputed(computedTpe.tpe.asType, function.asExpr.asInstanceOf[Expr[Any => Any]]),
Span.fromPosition(cfg.pos)
) :: Nil

Expand Down Expand Up @@ -114,7 +115,7 @@ private[ducktape] object Configuration {
Configuration.At.Successful(
path,
Target.Source,
Configuration.Computed(computedTpe.tpe.asType, function.asExpr.asInstanceOf[Expr[Any => Any]]),
Configuration.CaseComputed(computedTpe.tpe.asType, function.asExpr.asInstanceOf[Expr[Any => Any]]),
Span.fromPosition(cfg.pos)
) :: Nil

Expand Down Expand Up @@ -203,7 +204,7 @@ private[ducktape] object Configuration {
Configuration.At.Successful(
path,
Target.Source,
Configuration.Computed(Type.of[dest], function.asInstanceOf[Expr[Any => Any]]),
Configuration.CaseComputed(Type.of[dest], function.asInstanceOf[Expr[Any => Any]]),
Span.fromExpr(cfg)
) :: Nil

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import scala.quoted.*
private[ducktape] object PlanInterpreter {

def run[A: Type](plan: Plan[Nothing], sourceValue: Expr[A])(using Quotes): Expr[Any] =
recurse(plan, sourceValue)
recurse(plan, sourceValue)(using sourceValue)

private def recurse[A: Type](plan: Plan[Nothing], value: Expr[Any])(using Quotes): Expr[Any] = {
private def recurse[A: Type](plan: Plan[Nothing], value: Expr[Any])(using toplevelValue: Expr[A])(using Quotes): Expr[Any] = {
import quotes.reflect.*

plan match {
Expand All @@ -21,8 +21,10 @@ private[ducktape] object PlanInterpreter {
config match {
case Configuration.Const(value, _) =>
value
case Configuration.Computed(_, function) =>
case Configuration.CaseComputed(_, function) =>
'{ $function.apply($value) }
case Configuration.FieldComputed(_, function) =>
'{ $function.apply($toplevelValue) }
case Configuration.FieldReplacement(source, name, tpe) =>
source.accessFieldByName(name).asExpr
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import scala.quoted.*
private[ducktape] object Planner {
import Structure.*

def between(source: Structure, dest: Structure)(using Quotes) = {
def between(source: Structure, dest: Structure)(using Quotes, TransformationSite) = {
given Depth = Depth.zero
recurse(source, dest, Path.empty(source.tpe), Path.empty(dest.tpe))
}
Expand All @@ -18,7 +18,7 @@ private[ducktape] object Planner {
dest: Structure,
sourceContext: Path,
destContext: Path
)(using quotes: Quotes, depth: Depth): Plan[Plan.Error] = {
)(using quotes: Quotes, depth: Depth, transformationSite: TransformationSite): Plan[Plan.Error] = {
import quotes.reflect.*
given Depth = Depth.incremented(using depth)

Expand Down Expand Up @@ -99,7 +99,7 @@ private[ducktape] object Planner {
dest: Structure.Product,
sourceContext: Path,
destContext: Path
)(using Quotes, Depth) = {
)(using Quotes, Depth, TransformationSite) = {
val fieldPlans = dest.fields.map { (destField, destFieldStruct) =>
val updatedDestContext = destContext.appended(Path.Segment.Field(destFieldStruct.tpe, destField))
val plan =
Expand Down Expand Up @@ -129,7 +129,7 @@ private[ducktape] object Planner {
dest: Structure.Function,
sourceContext: Path,
destContext: Path
)(using Quotes, Depth) = {
)(using Quotes, Depth, TransformationSite) = {
val argPlans = dest.args.map { (destField, destFieldStruct) =>
val updatedDestContext = destContext.appended(Path.Segment.Field(destFieldStruct.tpe, destField))
val plan =
Expand Down Expand Up @@ -159,7 +159,7 @@ private[ducktape] object Planner {
dest: Structure.Coproduct,
sourceContext: Path,
destContext: Path
)(using Quotes, Depth) = {
)(using Quotes, Depth, TransformationSite) = {
val casePlans = source.children.map { (sourceName, sourceCaseStruct) =>
val updatedSourceContext = sourceContext.appended(Path.Segment.Case(sourceCaseStruct.tpe))

Expand All @@ -184,15 +184,20 @@ private[ducktape] object Planner {
}

object UserDefinedTransformation {
def unapply(structs: (Structure, Structure))(using Quotes, Depth): Option[Expr[Transformer[?, ?]]] = {
def unapply(structs: (Structure, Structure))(using Quotes, Depth, TransformationSite): Option[Expr[Transformer[?, ?]]] = {
val (src, dest) = structs

// if current depth is lower or equal to 1 then that means we're most likely referring to ourselves
if Depth.current <= 1 then None
else
def summonTransformer =
(src.tpe -> dest.tpe) match {
case '[src] -> '[dest] => Expr.summon[Transformer[src, dest]]
}

// if current depth is lower or equal to 1 then that means we're most likely referring to ourselves
summon[TransformationSite] match {
case TransformationSite.Definition if Depth.current <= 1 => None
case TransformationSite.Definition => summonTransformer
case TransformationSite.Transformation => summonTransformer
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package io.github.arainko.ducktape.internal

import scala.quoted.FromExpr
import scala.quoted.Expr
import scala.quoted.Quotes

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
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,16 @@ import scala.quoted.runtime.StopMacroExpansion
private[ducktape] object Transformations {
inline def between[A, B](
value: A,
inline transformationSite: TransformationSite,
inline configs: Field[A, B] | Case[A, B]*
): B = ${ createTransformationBetween[A, B]('value, 'configs) }
): B = ${ createTransformationBetween[A, B]('value, 'transformationSite, 'configs) }

private def createTransformationBetween[A: Type, B: Type](
value: Expr[A],
transformationSite: Expr[TransformationSite],
configs: Expr[Seq[Field[A, B] | Case[A, B]]]
)(using Quotes): Expr[B] = {
given TransformationSite = transformationSite.valueOrAbort
val plan = Planner.between(Structure.of[A], Structure.of[B])
val config = Configuration.parse(configs)
createTransformation(value, plan, config).asExprOf[B]
Expand All @@ -24,20 +27,25 @@ private[ducktape] object Transformations {
inline def via[A, B, Func, Args <: FunctionArguments](
value: A,
function: Func,
inline transformationSite: TransformationSite,
inline configs: Field[A, Args] | Case[A, Args]*
): B = ${ createTransformationVia[A, B, Func, Args]('value, 'function, 'configs) }
): B = ${ createTransformationVia[A, B, Func, Args]('value, 'function, 'transformationSite, 'configs) }

transparent inline def viaInferred[A, Func, Args <: FunctionArguments](
value: A,
inline transformationSite: TransformationSite,
inline function: Func,
inline configs: Field[A, Args] | Case[A, Args]*
): Any = ${ createTransformationViaInferred('value, 'function, 'configs) }
): Any = ${ createTransformationViaInferred('value, 'function, 'transformationSite, 'configs) }

private def createTransformationViaInferred[A: Type, Func: Type, Args <: FunctionArguments: Type](
value: Expr[A],
function: Expr[Func],
transformationSite: Expr[TransformationSite],
configs: Expr[Seq[Field[A, Args] | Case[A, Args]]]
)(using Quotes) = {
given TransformationSite = transformationSite.valueOrAbort

val plan =
Function
.fromExpr(function)
Expand All @@ -60,8 +68,11 @@ 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],
configs: Expr[Seq[Field[A, Args] | Case[A, Args]]]
)(using Quotes) = {
given TransformationSite = transformationSite.valueOrAbort

val plan =
Function
.fromFunctionArguments[Args, Func](function)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
package io.github.arainko.ducktape

import io.github.arainko.ducktape.internal.Transformations
import io.github.arainko.ducktape.internal.{Transformations, TransformationSite}

extension [Source](source: Source) {
inline def to[Dest]: Dest = Transformations.between[Source, Dest](source)
inline def to[Dest]: Dest = Transformations.between[Source, Dest](source, TransformationSite.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, function)
Transformations.viaInferred[Source, Func, Nothing](source, TransformationSite.Transformation, function)

transparent inline def intoVia[Func](inline function: Func): Any =
AppliedViaBuilder.create(source, function)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,6 @@ class AppliedBuilderSuite extends DucktapeSuite {
"Configuration is not valid since the provided type (java.lang.String) is not a subtype of scala.Int @ TestClassWithAdditionalGenericArg[Int].additionalArg"
)
}

}

object AppliedBuilderSuite {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -389,4 +389,5 @@ class DerivedTransformerSuite extends DucktapeSuite {
val actual = source.to[Dest]
assertEquals(actual, expected)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,38 @@ class NestedConfigurationSuite extends DucktapeSuite {
)
}: @nowarn("msg=unused local definition")

test("Field.computed works for nested fields") {
final case class SourceToplevel1(level1: SourceLevel1)
final case class SourceLevel1(level2: SourceLevel2)
final case class SourceLevel2(int: Int)

final case class DestToplevel1(level1: DestLevel1)
final case class DestLevel1(level2: DestLevel2)
final case class DestLevel2(int: Int, extra: String)

val source = SourceToplevel1(SourceLevel1(SourceLevel2(1)))
val expected = DestToplevel1(DestLevel1(DestLevel2(1, "1CONF")))

assertEachEquals(
source
.into[DestToplevel1]
.transform(
Field.computed(_.level1.level2.extra, a => a.level1.level2.int.toString() + "CONF")
),
source
.intoVia(DestToplevel1.apply)
.transform(Field.computed(_.level1.level2.extra, a => a.level1.level2.int.toString() + "CONF")),
Transformer
.define[SourceToplevel1, DestToplevel1]
.build(Field.computed(_.level1.level2.extra, a => a.level1.level2.int.toString() + "CONF"))
.transform(source),
Transformer
.defineVia[SourceToplevel1](DestToplevel1.apply)
.build(Field.computed(_.level1.level2.extra, a => a.level1.level2.int.toString() + "CONF"))
.transform(source)
)(expected)
}

test("nested coproduct cases can be configured") {
enum SourceToplevel1 {
case Level1(level2: SourceLevel2)
Expand Down

0 comments on commit 21d783f

Please sign in to comment.