Transfomation rules

Let's go over the priority and rules that ducktape uses to create a transformation (in the same order they're tried in the implementation):

1. User supplied Transformers

Custom instances of a Transfomer are always prioritized since these also function as an extension mechanism of the library.

import io.github.arainko.ducktape.*

given Transformer[String, List[String]] = str => str :: Nil

"single value".to[List[String]]
// res0: List[String] = List("single value")
((MdocApp.this.given_Transformer_String_List.transform("single value"): List[String]): List[String])

2. Upcasting

Transforming a type to its supertype is just an upcast.

// (Int | String) >: Int
1.to[Int | String]
// res2: Int | String = 1
((1: Int | String): Int | String)

3. Flatmapping over an arbitrary F[_] with a fallible transformation underneath (fallible transformations only)

A value wrapped in an arbitrary F[_] can be flatmapped over given that:

case class Positive private (value: Int)

object Positive {
  given Transformer.Fallible[[a] =>> Either[String, a], Int, Positive] =
    int => if int < 0 then Left("Lesser or equal to 0") else Right(Positive(int))
}

Mode.FailFast.either[String].locally {
  Right(1).fallibleTo[Positive]
}
// res4: Either[String, Positive] = Right(value = Positive(value = 1))
{
  val self$proxy2: Either[String] = Mode.FailFast.either[String]
  {
    val value$proxy4: Right[Nothing, Int] = Right.apply[Nothing, Int](1)
    (self$proxy2.flatMap[Int, Positive](
      value$proxy4,
      (a: Int) => MdocApp.this.Positive.given_Fallible_Either_Int_Positive.transform(a)
    ): Either[String, Positive]): Either[String, Positive]
  }: Either[String, Positive]
}

4. Mapping over an arbitrary F[_] (fallible transformations only)

A value wrapped in an arbitrary F[_] can be mapped over given that:

Mode.FailFast.either[String].locally {
  Right(1).fallibleTo[Option[Int]]
}
// res6: Either[String, Option[Int]] = Right(value = Some(value = 1))
{
  val self$proxy4: Either[String] = Mode.FailFast.either[String]
  {
    val value$proxy7: Right[Nothing, Int] = Right.apply[Nothing, Int](1)
    (self$proxy4
      .map[Int, Option[Int]](value$proxy7, (a: Int) => Some.apply[Int](a)): Either[String, Option[Int]]): Either[String, Option[
      Int
    ]]
  }: Either[String, Option[Int]]
}

5. Mapping over an Option

Transforming between options comes down to mapping over it and recursively deriving a transformation for the value inside.

given Transformer[Int, String] = int => int.toString

Option(1).to[Option[String]]
// res8: Option[String] = Some(value = "1")
{
  val source$proxy2: Option[Int] = Option.apply[Int](1)
  (source$proxy2
    .map[String]((src: Int) => MdocApp.this.given_Transformer_Int_String.transform(src)): Option[String]): Option[String]
}

6. Transforming and wrapping in an Option

If a transformation between two types is possible then transforming between the source type and an Option of the destination type is just wrapping the transformation result in a Some.

1.to[Option[Int | String]]
// res10: Option[Int | String] = Some(value = 1)
((Some.apply[Int | String](1): Option[Int | String]): Option[Int | String])

7. Mapping over and changing the collection type

//`.to` is already a method on collections
import io.github.arainko.ducktape.to as convertTo

List(1, 2, 3, 4).convertTo[Vector[Int | String]]
// res12: Vector[Int | String] = Vector(1, 2, 3, 4)
{
  val source$proxy4: List[Int] = List.apply[Int](1, 2, 3, 4)
  (source$proxy4
    .map[Int | String]((src: Int) => src)
    .to[Vector[Int | String]](iterableFactory[Int | String]): Vector[Int | String]): Vector[Int | String]
}

8. Transforming between case classes

A source case class can be transformed into the destination case class given that:

import io.github.arainko.ducktape.*

case class SourceToplevel(level1: SourceLevel1)
case class SourceLevel1(extra: String, int: Int, level2s: List[SourceLevel2])
case class SourceLevel2(value: Int)

case class DestToplevel(level1: DestLevel1)
case class DestLevel1(int: Int | String, level2s: Vector[DestLevel2])
case class DestLevel2(value: Option[Int])

SourceToplevel(SourceLevel1("extra", 1, List(SourceLevel2(1), SourceLevel2(2)))).to[DestToplevel]
// res15: DestToplevel = DestToplevel(
//   level1 = DestLevel1(
//     int = 1,
//     level2s = Vector(
//       DestLevel2(value = Some(value = 1)),
//       DestLevel2(value = Some(value = 2))
//     )
//   )
// )
{
  val source$proxy6: SourceToplevel =
    SourceToplevel.apply(SourceLevel1.apply("extra", 1, List.apply[SourceLevel2](SourceLevel2.apply(1), SourceLevel2.apply(2))))
  (new DestToplevel(level1 =
    new DestLevel1(
      int = source$proxy6.level1.int,
      level2s = source$proxy6.level1.level2s
        .map[DestLevel2]((src: SourceLevel2) => new DestLevel2(value = Some.apply[Int](src.value)))
        .to[Vector[DestLevel2]](iterableFactory[DestLevel2])
    )
  ): DestToplevel): DestToplevel
}

9. Transforming between case classes and tuples

A source case class can be transformed into a tuple given that:

import io.github.arainko.ducktape.*

case class Source(field1: Int, field2: List[Int], field3: Int, field4: Int)

Source(1, List(2, 2, 2), 3, 4).to[(Int, Vector[Int], Option[Int])]
// res18: Tuple3[Int, Vector[Int], Option[Int]] = (
//   1,
//   Vector(2, 2, 2),
//   Some(value = 3)
// )
{
  val source$proxy8: Source = Source.apply(1, List.apply[Int](2, 2, 2), 3, 4)
  (Tuple3.apply[field1.type, Vector[Int], Some[Int]](
    source$proxy8.field1,
    source$proxy8.field2.map[Int]((src: Int) => src).to[Vector[Int]](iterableFactory[Int]),
    Some.apply[Int](source$proxy8.field3)
  ): Tuple3[Int | String, Vector[Int], Option[Int]]): Tuple3[Int | String, Vector[Int], Option[Int]]
}

10. Transforming between tuples and case classes

A source tuple can be transformed into a case class given that:

import io.github.arainko.ducktape.*

case class Dest(field1: Int, field2: List[Int], field3: Option[Int])

(1, Vector(2, 2, 2), 3, 4).to[Dest]
// res21: Dest = Dest(
//   field1 = 1,
//   field2 = List(2, 2, 2),
//   field3 = Some(value = 3)
// )
{
  val source$proxy10: Tuple4[Int, Vector[Int], Int, Int] =
    Tuple4.apply[Int, Vector[Int], Int, Int](1, Vector.apply[Int](2, 2, 2), 3, 4)
  (new Dest(
    source$proxy10._1,
    source$proxy10._2.map[Int]((src: Int) => src).to[List[Int]](iterableFactory[Int]),
    Some.apply[Int](source$proxy10._3)
  ): Dest): Dest
}

11. Transforming between tuples

A source tuple can be transformed into a destination tuple given that:

import io.github.arainko.ducktape.*

(1, Vector(2, 2, 2), 3, 4).to[(Int, List[Int], Option[Int])]
// res24: Tuple3[Int, List[Int], Option[Int]] = (
//   1,
//   List(2, 2, 2),
//   Some(value = 3)
// )
{
  val source$proxy12: Tuple4[Int, Vector[Int], Int, Int] =
    Tuple4.apply[Int, Vector[Int], Int, Int](1, Vector.apply[Int](2, 2, 2), 3, 4)
  (Tuple3.apply[_1.type, List[Int], Some[Int]](
    source$proxy12._1,
    source$proxy12._2.map[Int]((src: Int) => src).to[List[Int]](iterableFactory[Int]),
    Some.apply[Int](source$proxy12._3)
  ): Tuple3[Int, List[Int], Option[Int]]): Tuple3[Int, List[Int], Option[Int]]
}

12. Transforming between enums/sealed traits

A source coproduct can be transformed into the destination coproduct given that:

sealed trait PaymentMethod

object PaymentMethod {
  case class Card(name: String, digits: Long, expires: Long) extends PaymentMethod
  case object Cash extends PaymentMethod
  case class PayPal(email: String) extends PaymentMethod
}

enum OtherPaymentMethod {
  case Card(name: String, digits: Long, expires: Long)
  case PayPal(email: String)
  case Cash
  case FakeMoney
}

(PaymentMethod.Cash: PaymentMethod).to[OtherPaymentMethod]
// res26: OtherPaymentMethod = Cash
{
  val source$proxy14: PaymentMethod = PaymentMethod.Cash: PaymentMethod
  (if source$proxy14.isInstanceOf[Card] then
     new Card(
       name = source$proxy14.asInstanceOf[Card].name,
       digits = source$proxy14.asInstanceOf[Card].digits,
       expires = source$proxy14.asInstanceOf[Card].expires
     )
   else if source$proxy14.isInstanceOf[Cash.type] then Cash
   else if source$proxy14.isInstanceOf[PayPal] then new PayPal(email = source$proxy14.asInstanceOf[PayPal].email)
   else throw new RuntimeException("Unhandled case. This is most likely a bug in ducktape."): OtherPaymentMethod)
  : OtherPaymentMethod
}

13. Same named singletons

Transformations between same named singletons come down to just reffering to the destination singleton.

object example1 {
  case object Singleton
}

object example2 {
  case object Singleton
}

example1.Singleton.to[example2.Singleton.type]
// res28: Singleton = Singleton
Singleton

14. Unwrapping a value class

case class Wrapper1(value: Int) extends AnyVal

Wrapper1(1).to[Int]
// res30: Int = 1
{
  val source$proxy16: Wrapper1 = Wrapper1.apply(1)
  (source$proxy16.value: Int): Int
}

15. Wrapping a value class

case class Wrapper2(value: Int) extends AnyVal

1.to[Wrapper2]
// res32: Wrapper2 = Wrapper2(value = 1)
((new Wrapper2(1): Wrapper2): Wrapper2)

16. Automatically derived Transformer.Derived

Instances of Transformer.Derived are automatically derived as a fallback to support use cases where a generic type (eg. a field of a case class) is unknown at definition site.

Note that Transformer[A, B] <: Transformer.Derived[A, B] so any Transformer in scope is eligible to become a Transformer.Derived.

import io.github.arainko.ducktape.*

final case class Source[A](field1: Int, field2: String, generic: A)
final case class Dest[A](field1: Int, field2: String, generic: A)

def transformSource[A, B](source: Source[A])(using Transformer.Derived[A, B]): Dest[B] = source.to[Dest[B]]

transformSource[Int, Option[Int]](Source(1, "2", 3))
// res35: Dest[Option[Int]] = Dest(
//   field1 = 1,
//   field2 = "2",
//   generic = Some(value = 3)
// )
{
  def transformSource[A, B](source: Source[A])(using x$2: Transformer.Derived[A, B]): Dest[B] =
    (new Dest[B](field1 = source.field1, field2 = source.field2, generic = x$2.transform(source.generic)): Dest[B]): Dest[B]
  transformSource[Int, Option[Int]](Source.apply[Int](1, "2", 3))(
    new FromFunction[Int, Option[Int]]((value: Int) => Some.apply[Int](value): Option[Int]): Derived[Int, Option[Int]]
  )
}