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:

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

given Mode.Accumulating.Either[String, List]()

wirePerson.fallibleTo[domain.Person]
// res0: Either[List[String], Person] = Right(
//   value = Person(
//     firstName = "John",
//     lastName = "Doe",
//     paymentMethods = Vector(
//       Cash,
//       PayPal(email = "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.

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(
//   value = Person(
//     firstName = "John",
//     lastName = "Doe",
//     paymentMethods = Vector(
//       Cash,
//       PayPal(email = "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.

given Mode.Accumulating.Either[String, List]()

wirePerson.fallibleVia(domain.Person.apply)
// res4: Either[List[String], Person] = Right(
//   value = Person(
//     firstName = "John",
//     lastName = "Doe",
//     paymentMethods = Vector(
//       Cash,
//       PayPal(email = "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₆`)
  )
}
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(
//   value = 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[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.