Skip to content
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 @@ -17,6 +17,10 @@ private[ducktape] object Context {

transparent inline def current(using ctx: Context): ctx.type = ctx

extension [F <: Fallible](self: Context.Of[F]) {
inline def locally[A](inline f: Context.Of[F] ?=> A): A = f(using self)
}

case class PossiblyFallible[G[+x]](
wrapperType: WrapperType[G],
transformationSite: TransformationSite,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ private[ducktape] opaque type Depth <: Int = Int

private[ducktape] object Depth {
val zero: Depth = 0

def current(using depth: Depth): Depth = depth
def incremented(using depth: Depth): Depth = depth + 1

extension (self: Depth) {
def incremented: Depth = self + 1
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ private[ducktape] object FallibilityRefiner {
private def recurse[E <: Erroneous](plan: Plan[E, Fallible]): None.type | Unit =
boundary[None.type | Unit]:
plan match
case Upcast(source, dest) => ()
case Upcast(source, dest, _) => ()

case UserDefined(source, dest, transformer) =>
transformer match
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ private[ducktape] object FalliblePlanInterpreter {
Value.Unwrapped(PlanInterpreter.recurse[A](plan, value))
case None =>
plan match {
case Plan.Upcast(_, _) => Value.Unwrapped(value)
case Plan.Upcast(_, _, _) => Value.Unwrapped(value)

case Plan.Configured(_, _, config, _) =>
config match
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,11 @@ private[ducktape] sealed trait Plan[+E <: Erroneous, +F <: Fallible] {
private[ducktape] object Plan {
case class Upcast(
source: Structure,
dest: Structure
) extends Plan[Nothing, Nothing]
dest: Structure,
private val alternative: () => Plan[Erroneous, Nothing]
) extends Plan[Nothing, Nothing] {
lazy val alt = alternative()
}

case class UserDefined[+F <: Fallible](
source: Structure,
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ private[ducktape] object PlanInterpreter {
import quotes.reflect.*

plan match {
case Plan.Upcast(_, _) => value
case Plan.Upcast(_, _, _) => value

case Plan.Configured(_, _, config, _) =>
evaluateConfig(config, value)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import scala.util.boundary

private[ducktape] object Planner {
import Structure.*
private enum FallthroughUpcast {
case Yes, No
}

def between[F <: Fallible](source: Structure, dest: Structure)(using Quotes, Context.Of[F]) = {
given Depth = Depth.zero
Expand All @@ -19,10 +22,12 @@ private[ducktape] object Planner {

private def recurse[F <: Fallible](
source: Structure,
dest: Structure
dest: Structure,
// TODO: Come up with something nicer
noUpcast: FallthroughUpcast = FallthroughUpcast.No
)(using quotes: Quotes, depth: Depth, context: Context.Of[F]): Plan[Erroneous, F] = {
import quotes.reflect.*
given Depth = Depth.incremented(using depth)
given Depth = depth.incremented

Logger.loggedDebug(s"Plan @ depth ${Depth.current}"):
(source.force -> dest.force) match {
Expand All @@ -40,8 +45,9 @@ private[ducktape] object Planner {
case UserDefinedTransformation(transformer) =>
verifyNotSelfReferential(Plan.UserDefined(source, dest, transformer))

case (source, dest) if source.tpe.repr <:< dest.tpe.repr =>
Plan.Upcast(source, dest)
case (source, dest) if noUpcast == FallthroughUpcast.No && source.tpe.repr <:< dest.tpe.repr =>
// Don't allow fallible transformations in the alternative case
Plan.Upcast(source, dest, () => context.toTotal.locally(recurse(source, dest, FallthroughUpcast.Yes)))

case BetweenFallibles(plan) => plan

Expand Down Expand Up @@ -277,12 +283,14 @@ private[ducktape] object Planner {
PartialFunction.condOpt(Context.current *: structs) {
case (ctx: Context.PossiblyFallible[f], source @ Wrapped(tpe, _, path, underlying), dest) =>
// needed for the recurse call to return Plan[Erroneous, Nothing]
given Context.Total = ctx.toTotal
val plan = Plan.BetweenFallibleNonFallible(
source,
dest,
recurse(underlying, dest)
)
val plan =
ctx.toTotal.locally {
Plan.BetweenFallibleNonFallible(
source,
dest,
recurse(underlying, dest)
)
}

// the compiler needs a bit more encouragement to be sure that the plan we construct has a fallibility of F
// Context.PossiblyFallible is defined with a type F = Fallible so we can deduce that ctx.F =:= Fallible =:= F
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package io.github.arainko.ducktape.issues

import io.github.arainko.ducktape.*

class Issue179Suite extends DucktapeSuite {
private given Mode.Accumulating.Either[String, List]()

test("configuring a field on an identity transformation works") {
case class Person(int: Int, str: String)

val source = Person(1, "1")
val expected = Person(2, "1")

assertTransformsConfigured(source, expected)(
Field.const(_.int, 2)
)
}

test("configuring a field on an identity transformation works (fallible)") {
case class Person(int: Int, str: String)

val source = Person(1, "1")
val expected = Person(2, "1")

assertTransformsFallibleConfigured(source, Right(expected))(
Field.fallibleConst(_.int, Right(2))
)
}

test("configuring a field on a nested identity transformation works") {
case class Level1(level2: Level2)

case class Level2(level3: Level3)

case class Level3(int: Int)

val source = Level1(Level2(Level3(1)))
val expected = Level1(Level2(Level3(2)))

assertTransformsConfigured(source, expected)(
Field.const(_.level2.level3.int, 2)
)
}

test("configuring a field on a nested identity transformation works (fallible)") {
case class Level1(level2: Level2)

case class Level2(level3: Level3)

case class Level3(int: Int)

val source = Level1(Level2(Level3(1)))
val expected = Level1(Level2(Level3(2)))

assertTransformsFallibleConfigured(source, Right(expected))(
Field.fallibleConst(_.level2.level3.int, Right(2))
)
}

test("configuring a field on an identity transformation going through a coproduct transformation works") {
enum Coprod {
case One(int: Int)
case Two(int: Int)
}

val source = Coprod.One(1)
val expected = Coprod.One(2)

assertTransformsConfigured(source, expected)(
Field.const(_.at[Coprod.One].int, 2)
)
}

test("configuring a field on an identity transformation going through a coproduct transformation works (fallible)") {
enum Coprod {
case One(int: Int)
case Two(int: Int)
}

val source = Coprod.One(1)
val expected = Coprod.One(2)

assertTransformsFallibleConfigured(source, Right(expected))(
Field.fallibleConst(_.at[Coprod.One].int, Right(2))
)
}

test(
"configuring a field on an identity transformation going through an '.element' transformation (eg. OptionToOption) works"
) {
case class Level1(level2: Level2)

case class Level2(level3: Option[Level3])

case class Level3(int: Int)

val source = Level1(Level2(Some(Level3(1))))
val expected = Level1(Level2(Some(Level3(2))))

assertTransformsConfigured(source, expected)(
Field.const(_.level2.level3.element.int, 2)
)
}

test(
"configuring a field on an identity transformation going through an '.element' transformation (eg. OptionToOption) works (fallible)"
) {
case class Level1(level2: Level2)

case class Level2(level3: Option[Level3])

case class Level3(int: Int)

val source = Level1(Level2(Some(Level3(1))))
val expected = Level1(Level2(Some(Level3(2))))

assertTransformsFallibleConfigured(source, Right(expected))(
Field.fallibleConst(_.level2.level3.element.int, Right(2))
)
}

test("configuring a tuple element on an identity transformation works") {
val source = (1, 2, 3)
val expected = (1, 3, 3)

assertTransformsConfigured(source, expected)(
Field.const(_.apply(1), 3)
)
}

test("configuring a tuple element on an identity transformation works (fallible)") {
val source = (1, 2, 3)
val expected = (1, 3, 3)

assertTransformsFallibleConfigured(source, Right(expected))(
Field.fallibleConst(_.apply(1), Right(3))
)
}

test("configuring a case on an identity transformation works") {
enum Coprod {
case One(int: Int)
case Two(int: Int)
}

val source = Coprod.One(1)
val expected = Coprod.Two(1)

assertTransformsConfigured(source, expected)(
Case.computed(_.at[Coprod.One], src => Coprod.Two(src.int))
)
}

test("configuring a case on an identity transformation works (fallible)") {
enum Coprod {
case One(int: Int)
case Two(int: Int)
}

val source = Coprod.One(1)
val expected = Coprod.Two(1)

assertTransformsFallibleConfigured(source, Right(expected))(
Case.fallibleComputed(_.at[Coprod.One], src => Right(Coprod.Two(src.int)))
)
}
}