Basics
Entrypoint of the library
The user-facing API of ducktape
is mostly a bunch of extension methods that allow us to transform between types in a variety of ways, the only import needed to get started looks like this:
import io.github.arainko.ducktape.*
The import above brings in a number of extension methods, let's examine how these work by redefining a simplified version of the wire and domain models first seen in the motivating example:
object wire:
final case class Person(
firstName: String,
lastName: String,
paymentMethods: List[wire.PaymentMethod]
)
enum PaymentMethod:
case Card(name: String, digits: Long)
case PayPal(email: String)
case Cash
object domain:
final case class Person(
firstName: String,
lastName: String,
paymentMethods: Vector[domain.PaymentMethod]
)
enum PaymentMethod:
case PayPal(email: String)
case Card(digits: Long, name: String)
case Cash
...and creating an input instance of wire.Person
to be transformed into domain.Person
later down the line:
val wirePerson = wire.Person(
"John",
"Doe",
List(
wire.PaymentMethod.Cash,
wire.PaymentMethod.PayPal("john@doe.com"),
wire.PaymentMethod.Card("J. Doe", 23232323)
)
)
Using total transformations
Source#to[Dest]
- for any two typesSource
andDest
, used to create a direct transformation betweenSource
andDest
:
import io.github.arainko.ducktape.*
wirePerson.to[domain.Person]
// res0: Person = Person(
// firstName = "John",
// lastName = "Doe",
// paymentMethods = Vector(
// Cash,
// PayPal(email = "john@doe.com"),
// Card(digits = 23232323L, name = "J. Doe")
// )
// )
((new Person(
firstName = MdocApp.this.wirePerson.firstName,
lastName = MdocApp.this.wirePerson.lastName,
paymentMethods = MdocApp.this.wirePerson.paymentMethods
.map[PaymentMethod]((src: PaymentMethod) =>
if src.isInstanceOf[Card] then new Card(digits = src.asInstanceOf[Card].digits, name = src.asInstanceOf[Card].name)
else if src.isInstanceOf[PayPal] then new PayPal(email = src.asInstanceOf[PayPal].email)
else if src.isInstanceOf[Cash.type] then MdocApp.this.domain.PaymentMethod.Cash
else throw new RuntimeException("Unhandled case. This is most likely a bug in ducktape.")
)
.to[Vector[PaymentMethod]](iterableFactory[PaymentMethod])
): Person): Person)
Read more about the rules under which the transformations are generated in a chapter dedicated to transformation rules.
Source#into[Dest]
- for any two typesSource
andDest
, used to create a 'transformation builder' that allows fixing transformation errors and overriding transformations for selected fields or subtypes.
import io.github.arainko.ducktape.*
wirePerson
.into[domain.Person]
.transform(Field.const(_.paymentMethods.element.at[domain.PaymentMethod.PayPal].email, "overridden@email.com"))
// res2: Person = Person(
// firstName = "John",
// lastName = "Doe",
// paymentMethods = Vector(
// Cash,
// PayPal(email = "overridden@email.com"),
// Card(digits = 23232323L, name = "J. Doe")
// )
// )
{
val AppliedBuilder_this: AppliedBuilder[Person, Person] = into[Person](MdocApp.this.wirePerson)[MdocApp.this.domain.Person]
{
val value$proxy3: Person = AppliedBuilder_this.inline$value
new Person(
firstName = value$proxy3.firstName,
lastName = value$proxy3.lastName,
paymentMethods = value$proxy3.paymentMethods
.map[PaymentMethod]((src: PaymentMethod) =>
if src.isInstanceOf[Card] then new Card(digits = src.asInstanceOf[Card].digits, name = src.asInstanceOf[Card].name)
else if src.isInstanceOf[PayPal] then new PayPal(email = "overridden@email.com")
else if src.isInstanceOf[Cash.type] then MdocApp.this.domain.PaymentMethod.Cash
else throw new RuntimeException("Unhandled case. This is most likely a bug in ducktape.")
)
.to[Vector[PaymentMethod]](iterableFactory[PaymentMethod])
): Person
}: Person
}
Read more in the section about configuring transformations.
Source#via(<method reference>)
- for any typeSource
and a method reference that can be eta-expanded into a function with named arguments (which is subsequently used to expand the method's argument list with the fields of theSource
type):
import io.github.arainko.ducktape.*
wirePerson.via(domain.Person.apply)
// res4: Person = Person(
// firstName = "John",
// lastName = "Doe",
// paymentMethods = Vector(
// Cash,
// PayPal(email = "john@doe.com"),
// Card(digits = 23232323L, name = "J. Doe")
// )
// )
{
val firstName: String = MdocApp.this.wirePerson.firstName
val lastName: String = MdocApp.this.wirePerson.lastName
val paymentMethods: Vector[PaymentMethod] = MdocApp.this.wirePerson.paymentMethods
.map[PaymentMethod]((src: PaymentMethod) =>
if src.isInstanceOf[Card] then new Card(digits = src.asInstanceOf[Card].digits, name = src.asInstanceOf[Card].name)
else if src.isInstanceOf[PayPal] then new PayPal(email = src.asInstanceOf[PayPal].email)
else if src.isInstanceOf[Cash.type] then MdocApp.this.domain.PaymentMethod.Cash
else throw new RuntimeException("Unhandled case. This is most likely a bug in ducktape.")
)
.to[Vector[PaymentMethod]](iterableFactory[PaymentMethod])
MdocApp.this.domain.Person.apply(firstName, lastName, paymentMethods)
}
To read about how these transformations are generated head on over to the section about transformation rules.
Source.intoVia(<method reference>)
- for any typeSource
and a method reference that can be eta-expanded into a function with named arguments, used to create a 'transformation builder' that allows fixing transformation errors and overriding transformations for selected fields or subtypes.
import io.github.arainko.ducktape.*
wirePerson
.intoVia(domain.Person.apply)
.transform(Field.const(_.paymentMethods.element.at[domain.PaymentMethod.PayPal].email, "overridden@email.com"))
// res6: Person = Person(
// firstName = "John",
// lastName = "Doe",
// paymentMethods = Vector(
// Cash,
// PayPal(email = "overridden@email.com"),
// Card(digits = 23232323L, name = "J. Doe")
// )
// )
{
val inst: AppliedViaBuilder[Person, Nothing, Function3[String, String, Vector[PaymentMethod], Person], Nothing] =
inline$instance[Person, Function3[String, String, Vector[PaymentMethod], Person]](
MdocApp.this.wirePerson,
(firstName: String, lastName: String, paymentMethods: Vector[PaymentMethod]) =>
MdocApp.this.domain.Person.apply(firstName, lastName, paymentMethods)
)
val AppliedViaBuilder_this: AppliedViaBuilder[
Person,
Person,
Function3[String, String, Vector[PaymentMethod], Person],
FunctionArguments {
val firstName: String
val lastName: String
val paymentMethods: Vector[PaymentMethod]
}
] = inst.asInstanceOf[[args >: Nothing <: FunctionArguments, retTpe >: Nothing <: Any] =>> AppliedViaBuilder[
Person,
retTpe,
Function3[String, String, Vector[PaymentMethod], Person],
args
][
FunctionArguments {
val firstName: String
val lastName: String
val paymentMethods: Vector[PaymentMethod]
},
Person
]]
{
val value$proxy7: Person = AppliedViaBuilder_this.inline$value
val function$proxy12: Function3[String, String, Vector[PaymentMethod], Person] = AppliedViaBuilder_this.inline$function
function$proxy12.apply(
value$proxy7.firstName,
value$proxy7.lastName,
value$proxy7.paymentMethods
.map[PaymentMethod]((src: PaymentMethod) =>
if src.isInstanceOf[Card] then new Card(digits = src.asInstanceOf[Card].digits, name = src.asInstanceOf[Card].name)
else if src.isInstanceOf[PayPal] then new PayPal(email = "overridden@email.com")
else if src.isInstanceOf[Cash.type] then MdocApp.this.domain.PaymentMethod.Cash
else throw new RuntimeException("Unhandled case. This is most likely a bug in ducktape.")
)
.to[Vector[PaymentMethod]](iterableFactory[PaymentMethod])
): Person
}: Person
}
Read more in the section about configuring transformations.
To get an idea of what transformations are actually supported head on over to transformation rules.