Basics
Entrypoint
Much as in the case of total transformations the entry point of fallible transformations is just single import that brings in a bunch of extension methods:
import io.github.arainko.ducktape.*
        
        Introduction
Sometimes our domains are modeled with refinement types (i.e. instead of using a plain String we declare a NonEmptyString that exposes a smart constructor that enforces certain invariants throughout the app) and fallible transformations are specifically geared towards making that usecase as lighweight as possible. Let's introduce a wire/domain pair of models that makes use of this pattern:
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
          import newtypes.*
object domain:
  final case class Person(
    firstName: NonEmptyString,
    lastName: NonEmptyString,
    paymentMethods: Vector[domain.PaymentMethod]
  )
  enum PaymentMethod:
    case PayPal(email: NonEmptyString)
    case Card(digits: Positive, name: NonEmptyString)
    case Cash
          object newtypes:
  opaque type NonEmptyString <: String = String
  object NonEmptyString:
    def create(value: String): Either[String, NonEmptyString] =
      Either.cond(!value.isBlank, value, s"not a non-empty string")
// expand the 'create' method into an instance of Transformer.Fallible
// this is a key component in making those transformations automatic
    given failFast: Transformer.Fallible[[a] =>> Either[String, a], String, NonEmptyString] =
      create
// also declare the same fallible transformer but make it ready for error accumulation
    given accumulating: Transformer.Fallible[[a] =>> Either[List[String], a], String, NonEmptyString] =
      create(_).left.map(_ :: Nil)
  opaque type Positive <: Long = Long
  object Positive:
    def create(value: Long): Either[String, Positive] =
      Either.cond(value > 0, value, "not a positive long")
    given failFast: Transformer.Fallible[[a] =>> Either[String, a], Long, Positive] =
      create
    given accumulating: Transformer.Fallible[[a] =>> Either[List[String], a], Long, Positive] =
      create(_).left.map(_ :: Nil)
          ...and also an input value that we'll transform 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 fallible transformations
Before anything happens we've got to choose a Mode, i.e. a thing that dictates how the transformation gets expanded and what wrapper type will it use.
        There are two flavors of Modes:
Mode.Accumulatingfor error accumulation,Mode.FailFastfor the cases where we just want to bail at the very first sight of trouble.
These will be used interchangably throughout the examples below, but if you want to go more in depth on those head on over to definition of Mode
Source#fallibleTo[Dest]- for any two typesSourceandDest, used to create a direct transformation betweenSourceandDestbut taking into account all of the fallible transformations between the fields:
given Mode.Accumulating.Either[String, List]()
wirePerson.fallibleTo[domain.Person]
// res0: Either[List[String], Person] = Right(
//   Person(
//     firstName = "John",
//     lastName = "Doe",
//     paymentMethods = Vector(
//       Cash,
//       PayPal("john@doe.com"),
//       Card(digits = 23232323L, name = "J. Doe")
//     )
//   )
// )
          (({
  def transform(value: Tuple2[NonEmptyString, Tuple2[NonEmptyString, Vector[PaymentMethod]]]): Person =
    new Person(value._1, value._2._1, value._2._2)
  MdocApp.this.given_Either_String_List.map[Tuple2[NonEmptyString, Tuple2[NonEmptyString, Vector[PaymentMethod]]], Person](
    MdocApp.this.given_Either_String_List.product[NonEmptyString, Tuple2[NonEmptyString, Vector[PaymentMethod]]](
      MdocApp.this.newtypes.NonEmptyString.accumulating.transform(MdocApp.this.wirePerson.firstName),
      MdocApp.this.given_Either_String_List.product[NonEmptyString, Vector[PaymentMethod]](
        MdocApp.this.newtypes.NonEmptyString.accumulating.transform(MdocApp.this.wirePerson.lastName),
        MdocApp.this.given_Either_String_List
          .traverseCollection[PaymentMethod, PaymentMethod, [A >: Nothing <: Any] =>> Iterable[A][PaymentMethod], Vector[
            PaymentMethod
          ]](
            MdocApp.this.wirePerson.paymentMethods,
            (
              (a: PaymentMethod) =>
                if a.isInstanceOf[Card] then {
                  def `transform₂`(`value₂`: Tuple2[Positive, NonEmptyString]): Card = new Card(`value₂`._1, `value₂`._2)
                  MdocApp.this.given_Either_String_List.map[Tuple2[Positive, NonEmptyString], Card](
                    MdocApp.this.given_Either_String_List.product[Positive, NonEmptyString](
                      MdocApp.this.newtypes.Positive.accumulating.transform(a.asInstanceOf[Card].digits),
                      MdocApp.this.newtypes.NonEmptyString.accumulating.transform(a.asInstanceOf[Card].name)
                    ),
                    ((`value₃`: Tuple2[Positive, NonEmptyString]) => `transform₂`(`value₃`))
                  )
                } else if a.isInstanceOf[PayPal] then {
                  def `transform₃`(`value₄`: NonEmptyString): PayPal = new PayPal(`value₄`)
                  MdocApp.this.given_Either_String_List.map[NonEmptyString, PayPal](
                    MdocApp.this.newtypes.NonEmptyString.accumulating.transform(a.asInstanceOf[PayPal].email),
                    ((`value₅`: NonEmptyString) => `transform₃`(`value₅`))
                  )
                } else if a.isInstanceOf[Cash.type] then
                  MdocApp.this.given_Either_String_List.pure[PaymentMethod](MdocApp.this.domain.PaymentMethod.Cash)
                else throw new RuntimeException("Unhandled case. This is most likely a bug in ducktape.")
            )
          )(iterableFactory[PaymentMethod])
      )
    ),
    ((`value₆`: Tuple2[NonEmptyString, Tuple2[NonEmptyString, Vector[PaymentMethod]]]) => transform(`value₆`))
  )
}: Either[List[String], Person]): Either[List[String], Person])
          Read more about the rules under which the transformations are generated in a chapter dedicated to transformation rules.
Source#into[Dest].fallible- for any two typesSourceandDest, used to create a 'transformation builder' that allows fixing transformation errors and overriding transformations for selected fields or subtypes.
given Mode.FailFast.Either[String]()
wirePerson
  .into[domain.Person]
  .fallible
  .transform(
    Field.fallibleConst(
      _.paymentMethods.element.at[domain.PaymentMethod.PayPal].email,
      newtypes.NonEmptyString.create("overridden@email.com")
    )
  )
// res2: Either[String, Person] = Right(
//   Person(
//     firstName = "John",
//     lastName = "Doe",
//     paymentMethods = Vector(
//       Cash,
//       PayPal("overridden@email.com"),
//       Card(digits = 23232323L, name = "J. Doe")
//     )
//   )
// )
          {
  val Fallible_this: Fallible[[A >: Nothing <: Any] =>> Either[String, A], given_Either_String, Person, Person] =
    into[Person](MdocApp.this.wirePerson)[MdocApp.this.domain.Person]
      .fallible[[A >: Nothing <: Any] =>> Either[String, A], given_Either_String.type](given_Either_String)
  ({
    val source$proxy2: Person = Fallible_this.inline$source
    val F$proxy2: given_Either_String.type = Fallible_this.inline$F
    (F$proxy2.flatMap[NonEmptyString, Person](
      MdocApp.this.newtypes.NonEmptyString.failFast.transform(source$proxy2.firstName),
      (
        (field0: NonEmptyString) =>
          F$proxy2.flatMap[NonEmptyString, Person](
            MdocApp.this.newtypes.NonEmptyString.failFast.transform(source$proxy2.lastName),
            (
              (field1: NonEmptyString) =>
                F$proxy2.map[Vector[PaymentMethod], Person](
                  F$proxy2.traverseCollection[PaymentMethod, PaymentMethod, [A >: Nothing <: Any] =>> Iterable[A][
                    PaymentMethod
                  ], Vector[PaymentMethod]](
                    source$proxy2.paymentMethods,
                    (
                      (a: PaymentMethod) =>
                        if a.isInstanceOf[Card] then
                          F$proxy2.flatMap[Positive, Card](
                            MdocApp.this.newtypes.Positive.failFast.transform(a.asInstanceOf[Card].digits),
                            (
                              (`field0₂`: Positive) =>
                                F$proxy2.map[NonEmptyString, Card](
                                  MdocApp.this.newtypes.NonEmptyString.failFast.transform(a.asInstanceOf[Card].name),
                                  ((`field1₂`: NonEmptyString) => new Card(`field0₂`, `field1₂`))
                                )
                            )
                          )
                        else if a.isInstanceOf[PayPal] then
                          F$proxy2.map[NonEmptyString, PayPal](
                            MdocApp.this.newtypes.NonEmptyString.create("overridden@email.com"),
                            ((`field0₃`: NonEmptyString) => new PayPal(`field0₃`))
                          )
                        else if a.isInstanceOf[Cash.type] then
                          F$proxy2.pure[PaymentMethod](MdocApp.this.domain.PaymentMethod.Cash)
                        else throw new RuntimeException("Unhandled case. This is most likely a bug in ducktape.")
                    )
                  )(iterableFactory[PaymentMethod]),
                  ((field2: Vector[PaymentMethod]) => new Person(field0, field1, field2))
                )
            )
          )
      )
    ): Either[String, Person])
  }: Either[String, Person])
}
          Read more in the section about configuring fallible transformations.
Source#fallibleVia(<method reference>)- for any typeSourceand 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 theSourcetype):
given Mode.Accumulating.Either[String, List]()
wirePerson.fallibleVia(domain.Person.apply)
// res4: Either[List[String], Person] = Right(
//   Person(
//     firstName = "John",
//     lastName = "Doe",
//     paymentMethods = Vector(
//       Cash,
//       PayPal("john@doe.com"),
//       Card(digits = 23232323L, name = "J. Doe")
//     )
//   )
// )
          {
  def transform(value: Tuple2[NonEmptyString, Tuple2[NonEmptyString, Vector[PaymentMethod]]]): Person = {
    val firstName: NonEmptyString = value._1
    val lastName: NonEmptyString = value._2._1
    val paymentMethods: Vector[PaymentMethod] = value._2._2
    MdocApp.this.domain.Person.apply(firstName, lastName, paymentMethods)
  }
  given_Either_String_List.map[Tuple2[NonEmptyString, Tuple2[NonEmptyString, Vector[PaymentMethod]]], Person](
    given_Either_String_List.product[NonEmptyString, Tuple2[NonEmptyString, Vector[PaymentMethod]]](
      MdocApp.this.newtypes.NonEmptyString.accumulating.transform(MdocApp.this.wirePerson.firstName),
      given_Either_String_List.product[NonEmptyString, Vector[PaymentMethod]](
        MdocApp.this.newtypes.NonEmptyString.accumulating.transform(MdocApp.this.wirePerson.lastName),
        given_Either_String_List
          .traverseCollection[PaymentMethod, PaymentMethod, [A >: Nothing <: Any] =>> Iterable[A][PaymentMethod], Vector[
            PaymentMethod
          ]](
            MdocApp.this.wirePerson.paymentMethods,
            (
              (a: PaymentMethod) =>
                if a.isInstanceOf[Card] then {
                  def `transform₂`(`value₂`: Tuple2[Positive, NonEmptyString]): Card = new Card(`value₂`._1, `value₂`._2)
                  given_Either_String_List.map[Tuple2[Positive, NonEmptyString], Card](
                    given_Either_String_List.product[Positive, NonEmptyString](
                      MdocApp.this.newtypes.Positive.accumulating.transform(a.asInstanceOf[Card].digits),
                      MdocApp.this.newtypes.NonEmptyString.accumulating.transform(a.asInstanceOf[Card].name)
                    ),
                    ((`value₃`: Tuple2[Positive, NonEmptyString]) => `transform₂`(`value₃`))
                  )
                } else if a.isInstanceOf[PayPal] then {
                  def `transform₃`(`value₄`: NonEmptyString): PayPal = new PayPal(`value₄`)
                  given_Either_String_List.map[NonEmptyString, PayPal](
                    MdocApp.this.newtypes.NonEmptyString.accumulating.transform(a.asInstanceOf[PayPal].email),
                    ((`value₅`: NonEmptyString) => `transform₃`(`value₅`))
                  )
                } else if a.isInstanceOf[Cash.type] then
                  given_Either_String_List.pure[PaymentMethod](MdocApp.this.domain.PaymentMethod.Cash)
                else throw new RuntimeException("Unhandled case. This is most likely a bug in ducktape.")
            )
          )(iterableFactory[PaymentMethod])
      )
    ),
    ((`value₆`: Tuple2[NonEmptyString, Tuple2[NonEmptyString, Vector[PaymentMethod]]]) => transform(`value₆`))
  )
}
          Source.intoVia(<method reference>).fallible- for any typeSourceand 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.
given Mode.FailFast.Either[String]()
wirePerson
  .intoVia(domain.Person.apply)
  .fallible
  .transform(
    Field.fallibleConst(
      _.paymentMethods.element.at[domain.PaymentMethod.PayPal].email,
      newtypes.NonEmptyString.create("overridden@email.com")
    )
  )
// res6: Either[String, Person] = Right(
//   Person(
//     firstName = "John",
//     lastName = "Doe",
//     paymentMethods = Vector(
//       Cash,
//       PayPal("overridden@email.com"),
//       Card(digits = 23232323L, name = "J. Doe")
//     )
//   )
// )
          {
  val inst
    : AppliedViaBuilder[Person, Nothing, Function3[NonEmptyString, NonEmptyString, Vector[PaymentMethod], Person], Nothing] =
    inline$instance[Person, Function3[NonEmptyString, NonEmptyString, Vector[PaymentMethod], Person]](
      MdocApp.this.wirePerson,
      (
        (firstName: NonEmptyString, lastName: NonEmptyString, paymentMethods: Vector[PaymentMethod]) =>
          MdocApp.this.domain.Person.apply(firstName, lastName, paymentMethods)
      )
    )
  val $proxy2: newtypes {
    type Positive >: Long <: Long
    type NonEmptyString >: String <: String
  } = MdocApp.this.newtypes.$asInstanceOf$[
    newtypes {
      type Positive >: Long <: Long
      type NonEmptyString >: String <: String
    }
  ]
  val Fallible_this: Fallible[
    [A >: Nothing <: Any] =>> Either[String, A],
    given_Either_String,
    Person,
    Person,
    Function3[NonEmptyString, NonEmptyString, Vector[PaymentMethod], Person],
    FunctionArguments {
      val firstName: NonEmptyString
      val lastName: NonEmptyString
      val paymentMethods: Vector[PaymentMethod]
    }
  ] = inst
    .asInstanceOf[[args >: Nothing <: FunctionArguments,
    retTpe >: Nothing <: Any] =>> AppliedViaBuilder[Person, retTpe, Function3[NonEmptyString, NonEmptyString, Vector[
      PaymentMethod
    ], Person], args][
      FunctionArguments {
        val firstName: NonEmptyString
        val lastName: NonEmptyString
        val paymentMethods: Vector[PaymentMethod]
      },
      Person
    ]]
    .fallible[[A >: Nothing <: Any] =>> Either[String, A], given_Either_String.type](given_Either_String)
  ({
    val value$proxy5: Person = Fallible_this.inline$source
    val function$proxy12: Function3[NonEmptyString, NonEmptyString, Vector[PaymentMethod], Person] =
      Fallible_this.inline$function
    val F$proxy4: given_Either_String.type = Fallible_this.inline$F
    (F$proxy4.map[Vector[PaymentMethod], Person](
      F$proxy4.traverseCollection[PaymentMethod, PaymentMethod, [A >: Nothing <: Any] =>> Iterable[A][PaymentMethod], Vector[
        PaymentMethod
      ]](
        value$proxy5.paymentMethods,
        (
          (a: PaymentMethod) =>
            if a.isInstanceOf[Card] then
              F$proxy4.flatMap[Positive, Card](
                MdocApp.this.newtypes.Positive.failFast.transform(a.asInstanceOf[Card].digits),
                (
                  (field0: Positive) =>
                    F$proxy4.map[NonEmptyString, Card](
                      MdocApp.this.newtypes.NonEmptyString.failFast.transform(a.asInstanceOf[Card].name),
                      ((field1: NonEmptyString) => new Card(field0, field1))
                    )
                )
              )
            else if a.isInstanceOf[PayPal] then
              F$proxy4.map[NonEmptyString, PayPal](
                MdocApp.this.newtypes.NonEmptyString.create("overridden@email.com"),
                ((`field0₂`: NonEmptyString) => new PayPal(`field0₂`))
              )
            else if a.isInstanceOf[Cash.type] then F$proxy4.pure[PaymentMethod](MdocApp.this.domain.PaymentMethod.Cash)
            else throw new RuntimeException("Unhandled case. This is most likely a bug in ducktape.")
        )
      )(iterableFactory[PaymentMethod]),
      ((field2: Vector[PaymentMethod]) => function$proxy12.apply(value$proxy5.firstName, value$proxy5.lastName, field2))
    ): Either[String, Person])
  }: Either[String, Person])
}
          Read more in the section about configuring fallible transformations.