Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Issue 199] Add Field.computedDeep and Field.fallibleComputedDeep #220

Merged
merged 14 commits into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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` |

---

Expand Down Expand Up @@ -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** |
Expand Down
48 changes: 46 additions & 2 deletions docs/total_transformations/configuring_transformations.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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)
```
Expand Down
12 changes: 12 additions & 0 deletions ducktape/src/main/scala/io/github/arainko/ducktape/Field.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ 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]
): 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] = ???

Expand All @@ -31,6 +37,12 @@ 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
): 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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

}
Original file line number Diff line number Diff line change
Expand Up @@ -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] {
Expand Down Expand Up @@ -74,6 +73,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
Expand Down Expand Up @@ -173,6 +190,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +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 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 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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ 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
Expand All @@ -52,6 +53,16 @@ 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
Expand All @@ -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] =>
Expand Down Expand Up @@ -95,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
Expand Down Expand Up @@ -232,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 {
Expand Down Expand Up @@ -275,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)
}
}

Expand Down
Loading