Lifted Transformers (deprecated)
Warning
This feature is deprecated and most likely will be removed soon. Consider using Partial transformers instead.
While Chimney transformers wrap total functions of type From => To, they don’t
really support partial transformations, where depending on the input value, transformation
may succeed or fail.
Let’s take a look at the following example.
case class RegistrationForm(email: String,
username: String,
password: String,
age: String)
case class RegisteredUser(email: String,
username: String,
passwordHash: String,
age: Int)
We get field age: String as an input, but we would like to parse it into correct Int
or signal an error, if provided value is not valid integer. This is simply not possible
with total Transformer. This is a moment when lifted transformers, provided
by TransformerF type class come into play.
val okForm = RegistrationForm("john@example.com", "John", "s3cr3t", "40")
okForm
.intoF[Option, RegisteredUser] // (1)
.withFieldComputed(_.passwordHash, form => hashpw(form.password))
.withFieldComputedF(_.age, _.age.toIntOption) // (2)
.transform // (3)
// Some(RegisteredUser("john@example.com", "John", "...", 40)): Option[RegisteredUser]
There are few differences to total transformers in the example above:
Instead of
into[RegisteredUser], we useintoF[Option, RegisteredUser], which tells Chimney thatOptiontype will be used for handling partial transformations.Instead of
withFieldComputed, we usewithFieldComputedF, where second parameter is a function that wraps result into a type constructor provided in (1) -Optionin this case.Result type of
transformcall is notRegisteredUser, butOption[RegisteredUser].
As you expect, when provided age which is not valid integer, this code evaluates to None.
val badForm = RegistrationForm("john@example.com", "John", "s3cr3t", "not an int")
badForm
.intoF[Option, RegisteredUser]
.withFieldComputed(_.passwordHash, form => hashpw(form.password))
.withFieldComputedF(_.age, _.age.toIntOption)
.transform
// None: Option[RegisteredUser]
Lifted DSL operations
Similar to withFieldConst, withFieldComputed, withCoproductInstance operations in DSL,
there are lifted counterparts available:
withFieldConstFwithFieldComputedFwithCoproductInstanceF
Analogously to Transformer definition DSL for Transformer, we can define above transformation
as implicit TransformerF[Option, RegistrationForm, RegisteredUser]. In order to do this,
we use TransformerF.define (or equivalently Transformer.defineF).
implicit val transformer: TransformerF[Option, RegistrationForm, RegisteredUser] =
TransformerF.define[Option, RegistrationForm, RegisteredUser]
.withFieldComputed(_.passwordHash, form => hashpw(form.password))
.withFieldComputedF(_.age, _.age.toIntOption)
.buildTransformer
As commonly, as with total transformers, this instance may be later picked up and used other, lifted transformations. In the following example it’s used for transforming array of registration forms into list of registered users.
Array(okForm, badForm).transformIntoF[Option, List[RegisteredUser]]
// None: Option[List[RegisteredUser]]
Note that following error handling semantics for collections, we’ve got None as a result
(because not all of array elements were valid forms, according to the defined lifted transformer).
Capturing validation errors
Usually, when partial transformation failed, we would like to know why it failed.
Thus, we must use different wrapper type than Option that allows to capture error information.
Chimney supports out of the box Either[C[E], +*], as the wrapper type, where
E- type of a single error occurrenceC[_]- collection type to store all the transformation errors (likeSeq,Vector,List, etc.)
If we pick error type as String (as validation error message) and collection as Vector,
we obtain wrapper type Either[Vector[String], +*].
Note
Type syntax with +* is only available with
kind-projector compiler plugin.
If you don’t want to (or can’t) use it, you may either use type-lambda with weird syntax:
({type L[+X] = Either[Vector[String], X]})#L
or define type alias:
type EitherVecStr[+X] = Either[Vector[String], X]
and use type EitherVecStr as a lifted wrapper type.
Let’s enhance our RegistrationForm to RegisteredUser lifted transformer with few
additional validation rules:
emailfield should contain@characteragemust be at least18years
implicit val transformer: TransformerF[EitherVecStr, RegistrationForm, RegisteredUser] = {
Transformer.defineF[EitherVecStr, RegistrationForm, RegisteredUser]
.withFieldComputedF(_.email, form => {
if(form.email.contains('@')) {
Right(form.email)
} else {
Left(Vector(s"${form.username}'s email: does not contain '@' character"))
}
})
.withFieldComputed(_.passwordHash, form => hashpw(form.password))
.withFieldComputedF(_.age, form => form.age.toIntOption match {
case Some(value) if value >= 18 => Right(value)
case Some(value) => Left(Vector(s"${form.username}'s age: must have at least 18 years"))
case None => Left(Vector(s"${form.username}'s age: invalid number"))
})
.buildTransformer
}
Then, trying to transform multiple registration forms, we can validate all them at once:
Array(
RegistrationForm("john_example.com", "John", "s3cr3t", "10"),
RegistrationForm("alice@example.com", "Alice", "s3cr3t", "19"),
RegistrationForm("bob@example.com", "Bob", "s3cr3t", "21.5")
).transformIntoF[EitherVecStr, List[RegisteredUser]]
// Left(
// Vector(
// "John's email: does not contain '@' character",
// "John's age: must have at least 18 years",
// "Bob's age: invalid number",
// )
// )
In case when all the provided forms are correct, we obtain requested collection of
registered users, wrapped in Right.
Array(
RegistrationForm("john@example.com", "John", "s3cr3t", "40"),
RegistrationForm("alice@example.com", "Alice", "s3cr3t", "19"),
RegistrationForm("bob@example.com", "Bob", "s3cr3t", "21")
).transformIntoF[EitherVecStr, List[RegisteredUser]]
// Right(
// List(
// RegisteredUser("john@example.com", "John", "...", 40)
// RegisteredUser("alice@example.com", "Alice", "...", 19),
// RegisteredUser("bob@example.com", "Bob", "...", 21)
// )
// )
Warning
Note that collection type where you gather errors is independent of any eventual collection types that takes part in the transformation.
For Either wrappers, Chimney supports practically any Scala standard collection
type, but depending on your choice, you may obtain different performance characteristics.
Thus, collections with reasonably fast concatenation should be preferred on the
error channel.
If you prefer to use Cats library, you might be interested in Validated support for lifted transformers.
TransformerF type class
Similar to the Transformer type class, Chimney defines a TransformerF type class,
which allows to express partial (lifted, wrapped) transformation of type From => F[To].
trait TransformerF[F[+_], From, To] {
def transform(src: From): F[To]
}
The whole library functionality that refers to total transformers, is also supported for lifted transformers. This especially means:
local implicit instances of
TransformerFare preferred in the first place, before deriving as instance by a macro (read more about it in Deriving lifted transformers)all the
enable/disableflags are respected by lifted transformersyou can customize lifted transformers using any operation described in Customizing transformers which works as well for total transformers, as for lifted ones
all the Standard transformers rules are provided for lifted transformers too
derivation for case classes, tuples, Java beans are supported too
Note
Note that for convenience of some operations, F is defined with as
covariant type constructor.
Supporting custom F[_]
Chimney provides pluggable interface that allows you to use your own
F[_] type constructor in lifted transformations.
The library defines TransformerFSupport type class, as follows.
trait TransformerFSupport[F[+_]] {
def pure[A](value: A): F[A]
def product[A, B](fa: F[A], fb: => F[B]): F[(A, B)]
def map[A, B](fa: F[A], f: A => B): F[B]
def traverse[M, A, B](it: Iterator[A], f: A => F[B])(implicit fac: Factory[B, M]): F[M]
}
Important
Chimney macros, during lifted transformer derivation, resolve implicit instance
of TransformerFSupport for requested wrapper type constructor and use it
in various places in emitted code.
In order to be able to use wrapper type of your choice, you need to implement
an instance of TransformerFSupport and put it as implicit term in the scope of usage.
For those familiar with applicative functors and traversable type classes, implementation of these methods should be obvious. Yet it gives some choice about semantics of error handling.
Chimney supports Option, Either and cats.data.Validated
(in Cats integration (deprecated)) just exactly by providing implicit instaces of
TransformerFSupport implemented for those wrapper types.
Error path support
Warning
Support for enhanced error paths is currently an experimental feature and we don’t guarantee it will be included in the next library versions in the same shape.
Chimney provides ability to trace errors in lifted transformers.
For using it you need to implement an instance of TransformerFErrorPathSupport
trait TransformerFErrorPathSupport[F[+_]] {
def addPath[A](fa: F[A], node: ErrorPathNode): F[A]
}
- There are 4 different types of of
ErrorPathNode: Accessorfor case class field or java bean getterIndexfor collection indexMapKeyfor map keyMapValuefor map value
In case if Chimney can resolve instance of TransformerFErrorPathSupport in scope of your
lifted transformer, each error in transformation will contain path of nodes to error location
- Out of box Chimney contains instance for Either[C[TransformationError[M]], +*], where
M- type of error messageC[_]- collection type to store all the transformation errors (like Seq, Vector, List, etc.)TransformationError- default implementation of error containing path
Let’s take a look at the following example:
type V[+A] = Either[List[TransformationError[String]], A]
implicit val intParse: TransformerF[V, String, Int] =
str => Try(str.toInt).toEither.left.map(_ => List(TransformationError(s"Can't parse int from '$str'")))
// Raw domain
case class RawData(id: String, links: List[RawLink])
case class RawLink(id: String, mapping: Map[RawLinkKey, RawLinkValue])
case class RawLinkKey(id: String)
case class RawLinkValue(value: String)
// Domain
case class Data(id: Int, links: List[Link])
case class Link(id: Int, mapping: Map[LinkKey, LinkValue])
case class LinkKey(id: Int)
case class LinkValue(value: Int)
val rawData = RawData(
"undefined",
List(RawLink("null", Map(RawLinkKey("error") -> RawLinkValue("invalid"))))
)
// Errors output
rawData.transformIntoF[V, Data] == Left(
List(
TransformationError(
"Can't parse int from undefined",
List(Accessor("id"))
),
TransformationError(
"Can't parse int from null",
List(Accessor("links"), Index(0), Accessor("id"))
),
TransformationError(
"Can't parse int from error",
List(
Accessor("links"),
Index(0),
Accessor("mapping"),
MapKey(RawLinkKey("error")),
Accessor("id")
)
),
TransformationError(
"Can't parse int from invalid",
List(
Accessor("links"),
Index(0),
Accessor("mapping"),
MapValue(RawLinkKey("error")),
Accessor("value")
)
)
)
)
// Using build in showErrorPath
def printError(err: TransformationError[String]): String =
s"${err.message} on ${err.showErrorPath}"
rawData.transformIntoF[V, Data].left.toOption.map(_.map(printError)) ==
Some(
List(
"Can't parse int from undefined on id",
"Can't parse int from null on links(0).id",
"Can't parse int from error on links(0).mapping.keys(RawLinkKey(error)).id",
"Can't parse int from invalid on links(0).mapping(RawLinkKey(error)).value"
)
)
Emitted code
Curious how the emitted code for lifted transformers looks like?
Let’s first refactor the transformation defined above, which is equivalent to the previous one, but with few functions extracted out - their implementation is not really important at this point.
def validateEmail(form: RegistrationForm): EitherVecStr[String] = ...
def computePasswordHash(form: RegistrationForm): String = ...
def validateAge(form: RegistrationForm): EitherVecStr[Int] = ...
implicit val transformer: TransformerF[EitherVecStr, RegistrationForm, RegisteredUser] = {
Transformer.defineF[EitherVecStr, RegistrationForm, RegisteredUser]
.withFieldComputedF(_.email, validateEmail)
.withFieldComputed(_.passwordHash, computePasswordHash)
.withFieldComputedF(_.age, validateAge)
.buildTransformer
}
The .buildTransformer call generates implementation of TransformerF, which is
semantically equivalent to the following, hand-crafted version.
implicit val transformer: TransformerF[EitherVecStr, RegistrationForm, RegisteredUser] = {
val tfs: TransformerFSupport[EitherVecStr] = ... // resolved implicit instance
new TransformerF[EitherVecStr, RegistrationForm, RegisteredUser] {
def transform(form: RegistrationForm): EitherVecStr[RegisteredUser] = {
tfs.map(
tfs.product(validateEmail(form), validateAge(form)),
{ case (email: String, age: Int) =>
RegisteredUser(
email,
form.username,
computePasswordHash(form.password),
age
)
}
)
}
}
}
tfs.product is used to combine results of successful validations into
a tuple type (email, age): (String, Int). In case that some validations
failed, validation errors are combined together also by tfs.product.
Then, if all validations passed, tfs.map transforms their results to
a target value of type RegisteredUser. Otherwise, tfs.map just
passes validation errors as a final result.
Note
only functions provided by
withFieldComputedFare working with the wrapper typeFremaining fields transformations (indentity transformer for
usernameand a function provided bywithFieldComputedforpassword) work without any wrapping withF
This strategy leads to generating particularly efficient code.
Deriving lifted transformers
When deriving a TransformerF[F, From, To] instance, where:
type
Fromconsists of some typeF1type
Toconsists of some typeT1F1inFromis a counterpart ofT1inTo
…we need to have transformation from F1 to T1 in order to be able to
derive requested TransformerF.
The rule is that:
we first check for function
F1 => F[T1]passed to lifted DSL operations (withFieldConstF,withFieldComputedF, etc.) or functionF1 => T1passed to total DSL operations (withFieldConst,withFieldComputed, etc.)whichever was found, it’s used in the first place
the last one passed in DSL for given field/type wins
then we look for implicit instances for
TransformerF[F, F1, T1]andTransformer[F1, T1]if both of them were found, ambiguity compilation error is reported
if only one of them was found, it’s used
we try to derive lifted
TransformerF[F, F1, T1]using library rules