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.Accumulating
for error accumulation,Mode.FailFast
for 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 typesSource
andDest
, used to create a direct transformation betweenSource
andDest
but 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(
// 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.
Source#into[Dest].fallible
- for any two typesSource
andDest
, 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(
// 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.
Source#fallibleVia(<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):
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₆`)
)
}
Source.intoVia(<method reference>).fallible
- 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.
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.