Given a pair of case classes, Source
and Target
, that have nested case classes, and at each level of nesting, the fields in Target
are unaligned subsets of the ones in Source
, is there a way to write a generic Shapeless-powered transform from Source
to Target
?
For example, given the following Internal
and External
classes:
object Internal {
case class User(
firstName: String,
lastName: String,
isAdmin: Boolean,
address: Address
)
case class Address(
street: String,
country: String,
blacklisted: Boolean
)
}
object External {
// Note that isAdmin is missing and the fields are jumbled
case class User(
lastName: String,
firstName: String,
address: Address
)
// blacklisted is gone
case class Address(
street: String,
country: String
)
}
I'd like to be able to do something like
val internalUser = Internal.User(
firstName = "Joe",
lastName = "Blow",
isAdmin = false,
address = Internal.Address(
street = "Sesame",
country = "U-S-A",
blacklisted = false
)
)
val externalUser = Transform.into[External.User](internalUser)
I have some code that takes care of selecting a subset and aligning the fields, but the recursion part is a bit more challenging:
import shapeless._, ops.hlist.Align, ops.hlist.SelectAll, SelectAll._
class Transform[T] {
// The fun stuff. Given an S, returns a T, if S has the right (subset of) fields
def apply[S, SR <: HList, TR <: HList](s: S)(
implicit
genS: LabelledGeneric.Aux[S, SR],
genT: LabelledGeneric.Aux[T, TR],
selectAll: SelectAll[SR, TR],
align: Align[SelectAll[SR, TR]#Out, TR]): T =
genT.from(align(selectAll(genS.to(s))))
}
object Transform {
// Convenience method for building an instance of `Transform`
def into[T] = new Transform[T]
}
I've taken a look at this SO question, but the answer there doesn't take care of the fact that the fields are un-aligned subsets of the other.
This was a fun exercise in piecing together the various primitives in shapeless to get to the result. The following has been tested with shapeless 2.3.3 with Scala 2.12.6 and 2.13.0-M5 ...
We can define a Transform
type class like so,
import shapeless._, ops.hlist.ZipWithKeys, ops.record.{ Keys, SelectAll, Values }
trait Transform[T, U] {
def apply(t: T): U
}
object Transform {
def into[U] = new MkTransform[U]
class MkTransform[U] {
def apply[T](t: T)(implicit tt: Transform[T, U]): U = tt(t)
}
// The identity transform
implicit def transformId[T]: Transform[T, T] =
new Transform[T, T] {
def apply(t: T): T = t
}
// Transform for HLists
implicit def transformHCons[H1, T1 <: HList, H2, T2 <: HList]
(implicit
th: Transform[H1, H2],
tt: Transform[T1, T2]
): Transform[H1 :: T1, H2 :: T2] =
new Transform[H1 :: T1, H2 :: T2] {
def apply(r: H1 :: T1): H2 :: T2 = th(r.head) :: tt(r.tail)
}
// Transform for types which have a LabelledGeneric representation as
// a shapeless record
implicit def transformGen
[T, U, TR <: HList, UR <: HList, UK <: HList, UV <: HList, TS <: HList]
(implicit
genT: LabelledGeneric.Aux[T, TR], // T <-> corresponding record
genU: LabelledGeneric.Aux[U, UR], // U <-> corresponding record
keysU: Keys.Aux[UR, UK], // Keys of the record for U
valuesU: Values.Aux[UR, UV], // Values of the record for U
selT: SelectAll.Aux[TR, UK, TS], // Select the values of the record of T
// corresponding to the keys of U
trans: Lazy[Transform[TS, UV]], // Transform the selected values
zipKeys: ZipWithKeys.Aux[UK, UV, UR], // Construct a new record of U from the
// transformed values
): Transform[T, U] =
new Transform[T, U] {
def apply(t: T): U = {
genU.from(zipKeys(trans.value(selT(genT.to(t)))))
}
}
}
The interesting case is transformGen
. The type variables T
and U
are the source and target types and are fixed at call sites. The remaining type variables are solved for sequentially, left to right, as the implicit arguments are resolved top to bottom ... in most cases, the final type argument of each implicit is solved given the preceding type arguments, and the solution flows right/down to the subsequent resolutions.
Note also the use of shapeless's Lazy
guarding the recursive implicit argument trans
. This is not strictly necessary for your example, but could be in more complex or recursive cases. Also note that in Scala 2.13.0-M5 and later trans
can instead be defined as a by-name implicit argument.
Now, given your definitions,
val internalUser = Internal.User(
firstName = "Joe",
lastName = "Blow",
isAdmin = false,
address = Internal.Address(
street = "Sesame",
country = "U-S-A",
blacklisted = false
)
)
the following works as desired,
val expectedExternalUser = External.User(
lastName = "Blow",
firstName = "Joe",
address = External.Address(
street = "Sesame",
country = "U-S-A",
)
)
val externalUser = Transform.into[External.User](internalUser)
assert(externalUser == expectedExternalUser)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With