Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Recursive transformation between nested case classes where the fields in the target are unaligned subsets of the source class

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.

like image 848
lloydmeta Avatar asked Aug 23 '18 06:08

lloydmeta


1 Answers

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)
like image 93
Miles Sabin Avatar answered Nov 15 '22 05:11

Miles Sabin