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

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.

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.

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.

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.