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] with {}

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")
//     )
//   )
// )
((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])
              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]) => new Card(digits = value._1, name = value._2)
              )
            else if (a.isInstanceOf[PayPal])
              MdocApp.this.given_Either_String_List.map[NonEmptyString, PayPal](
                MdocApp.this.newtypes.NonEmptyString.accumulating.transform(a.asInstanceOf[PayPal].email),
                (`value₂`: NonEmptyString) => new PayPal(email = `value₂`)
              )
            else if (a.isInstanceOf[Cash.type])
              MdocApp.this.given_Either_String_List.pure[Cash.type](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]]]) =>
    new Person(firstName = `value₃`._1, lastName = `value₃`._2._1, paymentMethods = `value₃`._2._2)
): 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] with {}

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),
      (firstName: NonEmptyString) =>
        F$proxy2.flatMap[NonEmptyString, Person](
          MdocApp.this.newtypes.NonEmptyString.failFast.transform(source$proxy2.lastName),
          (lastName: 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])
                      F$proxy2.flatMap[Positive, Card](
                        MdocApp.this.newtypes.Positive.failFast.transform(a.asInstanceOf[Card].digits),
                        (digits: Positive) =>
                          F$proxy2.map[NonEmptyString, Card](
                            MdocApp.this.newtypes.NonEmptyString.failFast.transform(a.asInstanceOf[Card].name),
                            (name: NonEmptyString) => new Card(digits = digits, name = name)
                          )
                      )
                    else if (a.isInstanceOf[PayPal])
                      F$proxy2.map[NonEmptyString, PayPal](
                        MdocApp.this.newtypes.NonEmptyString.create("overridden@email.com"),
                        (email: NonEmptyString) => new PayPal(email = email)
                      )
                    else if (a.isInstanceOf[Cash.type]) F$proxy2.pure[Cash.type](MdocApp.this.domain.PaymentMethod.Cash)
                    else throw new RuntimeException("Unhandled case. This is most likely a bug in ducktape.")
                )(iterableFactory[PaymentMethod]),
              (paymentMethods: Vector[PaymentMethod]) =>
                new Person(firstName = firstName, lastName = lastName, paymentMethods = paymentMethods)
            )
        )
    ): Either[String, Person]
  }: Either[String, Person]
}

Read more in the section about configuring fallible transformations.

given Mode.Accumulating.Either[String, List] with {}

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")
//     )
//   )
// )
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])
              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]) => new Card(digits = value._1, name = value._2)
              )
            else if (a.isInstanceOf[PayPal])
              given_Either_String_List.map[NonEmptyString, PayPal](
                MdocApp.this.newtypes.NonEmptyString.accumulating.transform(a.asInstanceOf[PayPal].email),
                (`value₂`: NonEmptyString) => new PayPal(email = `value₂`)
              )
            else if (a.isInstanceOf[Cash.type]) given_Either_String_List.pure[Cash.type](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]]]) => {
    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 Mode.FailFast.Either[String] with {}

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])
            F$proxy4.flatMap[Positive, Card](
              MdocApp.this.newtypes.Positive.failFast.transform(a.asInstanceOf[Card].digits),
              (digits: Positive) =>
                F$proxy4.map[NonEmptyString, Card](
                  MdocApp.this.newtypes.NonEmptyString.failFast.transform(a.asInstanceOf[Card].name),
                  (name: NonEmptyString) => new Card(digits = digits, name = name)
                )
            )
          else if (a.isInstanceOf[PayPal])
            F$proxy4.map[NonEmptyString, PayPal](
              MdocApp.this.newtypes.NonEmptyString.create("overridden@email.com"),
              (email: NonEmptyString) => new PayPal(email = email)
            )
          else if (a.isInstanceOf[Cash.type]) F$proxy4.pure[Cash.type](MdocApp.this.domain.PaymentMethod.Cash)
          else throw new RuntimeException("Unhandled case. This is most likely a bug in ducktape.")
      )(iterableFactory[PaymentMethod]),
      (`paymentMethods₂`: Vector[PaymentMethod]) =>
        function$proxy12.apply(value$proxy5.firstName, value$proxy5.lastName, `paymentMethods₂`)
    ): Either[String, Person]
  }: Either[String, Person]
}

Read more in the section about configuring fallible transformations.