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. 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]]
// res4: 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]
}
4. 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]]
// res6: Option[Int | String] = Some(value = 1)
((Some.apply[Int | String](1): Option[Int | String]): Option[Int | String])
5. 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]]
// res8: 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]
}
6. 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]
// res11: 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
}
7. 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]
// res13: OtherPaymentMethod = Cash
{
val source$proxy8: PaymentMethod = PaymentMethod.Cash: PaymentMethod
(if (source$proxy8.isInstanceOf[Card])
new Card(
name = source$proxy8.asInstanceOf[Card].name,
digits = source$proxy8.asInstanceOf[Card].digits,
expires = source$proxy8.asInstanceOf[Card].expires
)
else if (source$proxy8.isInstanceOf[Cash.type]) Cash
else if (source$proxy8.isInstanceOf[PayPal]) new PayPal(email = source$proxy8.asInstanceOf[PayPal].email)
else
throw new RuntimeException(
"Unhandled case. This is most likely a bug in ducktape."
): OtherPaymentMethod): OtherPaymentMethod
}
8. 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]
// res15: Singleton = Singleton
Singleton
9. Unwrapping a value class
case class Wrapper1(value: Int) extends AnyVal
Wrapper1(1).to[Int]
// res17: Int = 1
{
val source$proxy10: Wrapper1 = Wrapper1.apply(1)
(source$proxy10.value: Int): Int
}
10. Wrapping a value class
case class Wrapper2(value: Int) extends AnyVal
1.to[Wrapper2]
// res19: Wrapper2 = Wrapper2(value = 1)
((new Wrapper2(1): Wrapper2): Wrapper2)
11. 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
.
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))
// res21: 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]]
)
}