Configuring fallible transformations
Prelude
If we were to dissect how the types behind config options are structured, we'd see this:
opaque type Field[A, B] <: Field.Fallible[Nothing, A, B] = Field.Fallible[Nothing, A, B]
object Field {
opaque type Fallible[+F[+x], A, B] = Unit
}
Non-fallible config options are a subtype of fallible configs, which means that all the things mentioned in configuring transformations
are also applicable to fallible configurations (and should be read before diving into this doc).
Having said all that, let's declare a wire/domain model pair we'll be working on:
object wire:
case class Person(name: String, age: Long, socialSecurityNo: String)
import newtypes.*
object domain:
case class Person(name: NonEmptyString, age: Positive, socialSecurityNo: NonEmptyString)
import io.github.arainko.ducktape.*
object newtypes:
opaque type NonEmptyString <: String = String
object NonEmptyString:
def make(value: String): Either[String, NonEmptyString] =
Either.cond(!value.isBlank, value, s"not a non-empty string")
def makeAccumulating(value: String): Either[List[String], NonEmptyString] =
make(value).left.map(_ :: Nil)
given failFast: Transformer.Fallible[[a] =>> Either[String, a], String, NonEmptyString] =
make
given accumulating: Transformer.Fallible[[a] =>> Either[List[String], a], String, NonEmptyString] =
makeAccumulating
opaque type Positive <: Long = Long
object Positive:
def make(value: Long): Either[String, Positive] =
Either.cond(value > 0, value, "not a positive long")
def makeAccumulating(value: Long): Either[List[String], Positive] =
make(value).left.map(_ :: Nil)
given failFast: Transformer.Fallible[[a] =>> Either[String, a], Long, Positive] =
make
given accumulating: Transformer.Fallible[[a] =>> Either[List[String], a], Long, Positive] =
makeAccumulating
...and some input examples:
// this should trip up our validation
val bad = wire.Person(name = "", age = -1, socialSecurityNo = "SOCIALNO")
// this one should pass
val good = wire.Person(name = "ValidName", age = 24, socialSecurityNo = "SOCIALNO")
Product configurations
Name | Description |
---|---|
Field.fallibleConst |
a fallible variant of Field.const that allows for supplying values wrapped in an F |
Field.fallibleComputed |
a fallible variant of Field.computed that allows for supplying functions that return values wrapped in an F |
Field.fallibleComputedDeep |
a fallible variant of Field.computedDeep that allows for supplying functions that return values wrapped in an F |
Field.fallibleConst
- a fallible variant ofField.const
that allows for supplying values wrapped in anF
import io.github.arainko.ducktape.*
given Mode.Accumulating.Either[String, List]()
bad
.into[domain.Person]
.fallible
.transform(
Field.fallibleConst(_.name, NonEmptyString.makeAccumulating("ConstValidName")),
Field.fallibleConst(_.age, Positive.makeAccumulating(25))
)
// res0: Either[List[String], Person] = Right(
// value = Person(
// name = "ConstValidName",
// age = 25L,
// socialSecurityNo = "SOCIALNO"
// )
// )
{
val Fallible_this: Fallible[[A >: Nothing <: Any] =>> Either[List[String], A], given_Either_String_List, Person, Person] =
into[Person](MdocApp.this.bad)[MdocApp.this.domain.Person]
.fallible[[A >: Nothing <: Any] =>> Either[List[String], A], given_Either_String_List.type](given_Either_String_List)
{
val source$proxy2: Person = Fallible_this.inline$source
val F$proxy2: given_Either_String_List.type = Fallible_this.inline$F
{
def transform(value: Tuple2[NonEmptyString, Tuple2[Positive, NonEmptyString]]): Person =
new Person(value._1, value._2._1, value._2._2)
F$proxy2.map[Tuple2[NonEmptyString, Tuple2[Positive, NonEmptyString]], Person](
F$proxy2.product[NonEmptyString, Tuple2[Positive, NonEmptyString]](
MdocApp.this.newtypes.NonEmptyString.makeAccumulating("ConstValidName"),
F$proxy2.product[Positive, NonEmptyString](
MdocApp.this.newtypes.Positive.makeAccumulating(25L),
MdocApp.this.newtypes.NonEmptyString.accumulating.transform(source$proxy2.socialSecurityNo)
)
),
(`value₂`: Tuple2[NonEmptyString, Tuple2[Positive, NonEmptyString]]) => transform(`value₂`)
)
}: Either[List[String], Person]
}: Either[List[String], Person]
}
Field.fallibleComputed
- a fallible variant ofField.computed
that allows for supplying functions that return values wrapped in anF
given Mode.Accumulating.Either[String, List]()
bad
.into[domain.Person]
.fallible
.transform(
Field.fallibleComputed(_.name, uvp => NonEmptyString.makeAccumulating(uvp.name + "ConstValidName")),
Field.fallibleComputed(_.age, uvp => Positive.makeAccumulating(uvp.age + 25))
)
// res2: Either[List[String], Person] = Right(
// value = Person(
// name = "ConstValidName",
// age = 24L,
// socialSecurityNo = "SOCIALNO"
// )
// )
{
val Fallible_this: Fallible[[A >: Nothing <: Any] =>> Either[List[String], A], given_Either_String_List, Person, Person] =
into[Person](MdocApp.this.bad)[MdocApp.this.domain.Person]
.fallible[[A >: Nothing <: Any] =>> Either[List[String], A], given_Either_String_List.type](given_Either_String_List)
{
val source$proxy4: Person = Fallible_this.inline$source
val F$proxy4: given_Either_String_List.type = Fallible_this.inline$F
{
def transform(value: Tuple2[NonEmptyString, Tuple2[Positive, NonEmptyString]]): Person =
new Person(value._1, value._2._1, value._2._2)
F$proxy4.map[Tuple2[NonEmptyString, Tuple2[Positive, NonEmptyString]], Person](
F$proxy4.product[NonEmptyString, Tuple2[Positive, NonEmptyString]](
MdocApp.this.newtypes.NonEmptyString.makeAccumulating(source$proxy4.name.+("ConstValidName")),
F$proxy4.product[Positive, NonEmptyString](
MdocApp.this.newtypes.Positive.makeAccumulating(source$proxy4.age.+(25)),
MdocApp.this.newtypes.NonEmptyString.accumulating.transform(source$proxy4.socialSecurityNo)
)
),
(`value₂`: Tuple2[NonEmptyString, Tuple2[Positive, NonEmptyString]]) => transform(`value₂`)
)
}: Either[List[String], Person]
}: Either[List[String], Person]
}
Field.fallibleComputedDeep
- a fallible variant ofField.computedDeep
that allows for supplying functions that return values wrapped in anF
given Mode.Accumulating.Either[String, List]()
case class SourceToplevel1(level1: Option[SourceLevel1])
case class SourceLevel1(level2: Option[SourceLevel2])
case class SourceLevel2(int: Int)
case class DestToplevel1(level1: Option[DestLevel1])
case class DestLevel1(level2: Option[DestLevel2])
case class DestLevel2(int: Positive)
val source = SourceToplevel1(Some(SourceLevel1(Some(SourceLevel2(1)))))
source
.into[DestToplevel1]
.fallible
.transform(
Field.fallibleComputedDeep(
_.level1.element.level2.element.int,
// the type here cannot be inferred automatically and needs to be provided by the user,
// a nice compiletime error message is shown (with a suggestion on what the proper type to use is) otherwise
(value: Int) => Positive.makeAccumulating(value + 10L))
)
// res4: Either[List[String], DestToplevel1] = Right(
// value = DestToplevel1(
// level1 = Some(
// value = DestLevel1(level2 = Some(value = DestLevel2(int = 11L)))
// )
// )
// )
{
val Fallible_this
: Fallible[[A >: Nothing <: Any] =>> Either[List[String], A], given_Either_String_List, SourceToplevel1, DestToplevel1] =
into[SourceToplevel1](source)[DestToplevel1]
.fallible[[A >: Nothing <: Any] =>> Either[List[String], A], given_Either_String_List.type](given_Either_String_List)
{
val source$proxy6: SourceToplevel1 = Fallible_this.inline$source
val F$proxy6: given_Either_String_List.type = Fallible_this.inline$F
{
def transform(value: None | Some[DestLevel1]): DestToplevel1 = new DestToplevel1(value)
F$proxy6.map[None | Some[DestLevel1], DestToplevel1](
source$proxy6.level1 match {
case None =>
F$proxy6.pure[None.type](None)
case Some(value) =>
F$proxy6.map[DestLevel1, Some[DestLevel1]](
{
def `transform₂`(`value₂`: None | Some[DestLevel2]): DestLevel1 = new DestLevel1(`value₂`)
F$proxy6.map[None | Some[DestLevel2], DestLevel1](
`value₃`.level2 match {
case None =>
F$proxy6.pure[None.type](None)
case Some(value) =>
F$proxy6.map[DestLevel2, Some[DestLevel2]](
{
def `transform₃`(`value₄`: Positive): DestLevel2 = new DestLevel2(`value₄`)
F$proxy6.map[Positive, DestLevel2](
{
val `value₅`: Int = `value₆`.int
MdocApp.this.newtypes.Positive.makeAccumulating(`value₅`.+(10L))
},
(`value₇`: Positive) => `transform₃`(`value₇`)
)
},
(`value₈`: DestLevel2) => Some.apply[DestLevel2](`value₈`)
)
},
(`value₉`: None | Some[DestLevel2]) => `transform₂`(`value₉`)
)
},
(`value₁₀`: DestLevel1) => Some.apply[DestLevel1](`value₁₀`)
)
},
(`value₁₁`: None | Some[DestLevel1]) => transform(`value₁₁`)
)
}: Either[List[String], DestToplevel1]
}: Either[List[String], DestToplevel1]
}
Coproduct configurations
Name | Description |
---|---|
Case.fallibleConst |
a fallible variant of Case.const that allows for supplying values wrapped in an F |
Case.fallibleComputed |
a fallible variant of Case.computed that allows for supplying functions that return values wrapped in an F |
Let's define a wire enum (pretend that it's coming from... somewhere) and a domain enum that doesn't exactly align with the wire one.
object wire:
enum ReleaseKind:
case LP, EP, Single
object domain:
enum ReleaseKind:
case EP, LP
Case.fallibleConst
- a fallible variant ofCase.const
that allows for supplying values wrapped in anF
given Mode.FailFast.Either[String]()
wire.ReleaseKind.Single
.into[domain.ReleaseKind]
.fallible
.transform(
Case.fallibleConst(_.at[wire.ReleaseKind.Single.type], Left("Unsupported release kind"))
)
// res6: Either[String, ReleaseKind] = Left(value = "Unsupported release kind")
{
val Fallible_this: Fallible[[A >: Nothing <: Any] =>> Either[String, A], given_Either_String, ReleaseKind, ReleaseKind] =
into[ReleaseKind](wire.ReleaseKind.Single)[domain.ReleaseKind]
.fallible[[A >: Nothing <: Any] =>> Either[String, A], given_Either_String.type](given_Either_String)
{
val source$proxy8: ReleaseKind = Fallible_this.inline$source
val F$proxy8: given_Either_String.type = Fallible_this.inline$F
if source$proxy8.isInstanceOf[LP.type] then F$proxy8.pure[ReleaseKind](domain.ReleaseKind.LP)
else if source$proxy8.isInstanceOf[EP.type] then F$proxy8.pure[ReleaseKind](domain.ReleaseKind.EP)
else if source$proxy8.isInstanceOf[Single.type] then Left.apply[String, Nothing]("Unsupported release kind")
else throw new RuntimeException("Unhandled case. This is most likely a bug in ducktape."): Either[String, ReleaseKind]
}: Either[String, ReleaseKind]
}
Case.fallibleComputed
- a fallible variant ofCase.computed
that allows for supplying functions that return values wrapped in anF
given Mode.FailFast.Either[String]()
// Type inference is tricky with this one. The function being passed in needs to be typed with the exact expected type.
def handleSingle(value: wire.ReleaseKind): Either[String, domain.ReleaseKind] =
Left("It's a single alright, too bad we don't support it")
wire.ReleaseKind.Single
.into[domain.ReleaseKind]
.fallible
.transform(
Case.fallibleComputed(_.at[wire.ReleaseKind.Single.type], handleSingle)
)
// res8: Either[String, ReleaseKind] = Left(
// value = "It's a single alright, too bad we don't support it"
// )
{
val Fallible_this: Fallible[[A >: Nothing <: Any] =>> Either[String, A], given_Either_String, ReleaseKind, ReleaseKind] =
into[ReleaseKind](wire.ReleaseKind.Single)[domain.ReleaseKind]
.fallible[[A >: Nothing <: Any] =>> Either[String, A], given_Either_String.type](given_Either_String)
{
val source$proxy10: ReleaseKind = Fallible_this.inline$source
val F$proxy10: given_Either_String.type = Fallible_this.inline$F
if source$proxy10.isInstanceOf[LP.type] then F$proxy10.pure[ReleaseKind](domain.ReleaseKind.LP)
else if source$proxy10.isInstanceOf[EP.type] then F$proxy10.pure[ReleaseKind](domain.ReleaseKind.EP)
else if source$proxy10.isInstanceOf[Single.type] then {
val value: ReleaseKind = source$proxy10.asInstanceOf[Single.type]
handleSingle(value)
} else throw new RuntimeException("Unhandled case. This is most likely a bug in ducktape."): Either[String, ReleaseKind]
}: Either[String, ReleaseKind]
}
Building custom instances of fallible transformers
Life is not always lolipops and crisps and sometimes you need to write a typeclass instance by hand. Worry not though, just like in the case of total transformers, we can easily define custom instances with the help of the configuration DSL (which, let's write it down once again, is a superset of total transformers' DSL).
By all means go wild with the configuration options, I'm too lazy to write them all out here again.
given Mode.Accumulating.Either[String, List]()
val customAccumulating =
Transformer
.define[wire.Person, domain.Person]
.fallible
.build(
Field.fallibleConst(_.name, NonEmptyString.makeAccumulating("IAmAlwaysValidNow!"))
)
{
val Fallible_this: Fallible[[A >: Nothing <: Any] =>> Either[List[String], A], given_Either_String_List, Person, Person] =
Transformer
.define[wire.Person, domain.Person]
.fallible[[A >: Nothing <: Any] =>> Either[List[String], A], given_Either_String_List.type](given_Either_String_List)
new FromFunction[[A >: Nothing <: Any] =>> Either[List[String], A], Person, Person]((source: Person) => {
val F$proxy12: given_Either_String_List.type = Fallible_this.inline$F
{
def transform(value: Tuple2[NonEmptyString, Tuple2[Positive, NonEmptyString]]): Person =
new Person(value._1, value._2._1, value._2._2)
F$proxy12.map[Tuple2[NonEmptyString, Tuple2[Positive, NonEmptyString]], Person](
F$proxy12.product[NonEmptyString, Tuple2[Positive, NonEmptyString]](
MdocApp.this.newtypes.NonEmptyString.makeAccumulating("IAmAlwaysValidNow!"),
F$proxy12.product[Positive, NonEmptyString](
MdocApp.this.newtypes.Positive.accumulating.transform(source.age),
MdocApp.this.newtypes.NonEmptyString.accumulating.transform(source.socialSecurityNo)
)
),
(`value₂`: Tuple2[NonEmptyString, Tuple2[Positive, NonEmptyString]]) => transform(`value₂`)
)
}: Either[List[String], Person]
}): Fallible[[A >: Nothing <: Any] =>> Either[List[String], A], Person, Person]
}
And for the ones that are not keen on writing out method arguments:
given Mode.Accumulating.Either[String, List]()
val customAccumulatingVia =
Transformer
.defineVia[wire.Person](domain.Person.apply)
.fallible
.build(
Field.fallibleConst(_.name, NonEmptyString.makeAccumulating("IAmAlwaysValidNow!"))
)
{
val $proxy2: DefinitionViaBuilder {
type PartiallyApplied >: [Source >: Nothing <: Any] =>> Unit <: [Source >: Nothing <: Any] =>> Unit
} = DefinitionViaBuilder.$asInstanceOf$[
DefinitionViaBuilder {
type PartiallyApplied >: [Source >: Nothing <: Any] =>> Unit <: [Source >: Nothing <: Any] =>> Unit
}
]
val DefinitionViaBuilder$_this: $proxy2.type = $proxy2
val partial$proxy2: PartiallyApplied[Person] & PartiallyApplied[Person] =
Transformer.defineVia[wire.Person].$asInstanceOf$[PartiallyApplied[Person] & PartiallyApplied[Person]]
val builder: DefinitionViaBuilder[Person, Nothing, Function3[NonEmptyString, Positive, NonEmptyString, Person], Nothing] =
DefinitionViaBuilder$_this.inline$instance[Person, Function3[NonEmptyString, Positive, NonEmptyString, Person]](
(name: NonEmptyString, age: Positive, socialSecurityNo: NonEmptyString) =>
domain.Person.apply(name, age, socialSecurityNo)
)
val $proxy5: DefinitionViaBuilder {
type PartiallyApplied >: [Source >: Nothing <: Any] =>> Unit <: [Source >: Nothing <: Any] =>> Unit
} = DefinitionViaBuilder.$asInstanceOf$[
DefinitionViaBuilder {
type PartiallyApplied >: [Source >: Nothing <: Any] =>> Unit <: [Source >: Nothing <: Any] =>> Unit
}
]
val $proxy6: 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[List[String], A],
given_Either_String_List,
Person,
Person,
Function3[NonEmptyString, Positive, NonEmptyString, Person],
FunctionArguments {
val name: NonEmptyString
val age: Positive
val socialSecurityNo: NonEmptyString
}
] = builder
.asInstanceOf[[args >: Nothing <: FunctionArguments, retTpe >: Nothing <: Any] =>> DefinitionViaBuilder[
Person,
retTpe,
Function3[NonEmptyString, Positive, NonEmptyString, Person],
args
][
FunctionArguments {
val name: NonEmptyString
val age: Positive
val socialSecurityNo: NonEmptyString
},
Person
]]
.fallible[[A >: Nothing <: Any] =>> Either[List[String], A], given_Either_String_List.type](given_Either_String_List)
new FromFunction[[A >: Nothing <: Any] =>> Either[List[String], A], Person, Person]((value: Person) => {
val function$proxy6: Function3[NonEmptyString, Positive, NonEmptyString, Person] = Fallible_this.inline$function
val F$proxy14: given_Either_String_List.type = Fallible_this.inline$F
{
def transform(`value₂`: NonEmptyString): Person = function$proxy6.apply(`value₂`, value.age, value.socialSecurityNo)
F$proxy14.map[NonEmptyString, Person](
MdocApp.this.newtypes.NonEmptyString.makeAccumulating("IAmAlwaysValidNow!"),
(`value₃`: NonEmptyString) => transform(`value₃`)
)
}: Either[List[String], Person]
}): Fallible[[A >: Nothing <: Any] =>> Either[List[String], A], Person, Person]
}