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:
- an instance of
Mode.FailFast[F]
is in scope, - a fallible transformation can be derived for the type being wrapped
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:
- an instance of
Mode[F]
is in scope, - a transformation can be derived for the type being wrapped
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:
- source has fields whose names cover all of the destination's fields,
- a transformation for the types corresponding to those fields can be derived.
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:
- the order of its fields align with the tuple's elements (the source's length CAN be greater than the destination tuple's length),
- a transformation for the types corresponding to the aligned fields and elements can be derived
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:
- the order of its elements align with the case class' fields (the source's length CAN be greater than the destination's length),
- a transformation for the types corresponding to the aligned fields and elements can be derived
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:
- the order of its elements align with the destination tuple's elements (the source's length CAN be greater than the destination's length),
- a transformation for the types corresponding to the aligned fields and elements can be derived
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:
- destination's children have names that match all of the source's children,
- a transformation between those two corresponding types can be derived.
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]]
)
}