Thomas
Thomas

Reputation: 1219

Scala: From nested case class to flatten case class

The question is:

How to build a general function that can take any case class that is composed of other case classes and flatten that into one case class with all values from each case class in the composed case class?

For example, I would like to convert a nested case class like this:

case class A(first: String, second: String)
case class B(value: String)

case class Nested(a: A, b: B)

to a flatten case class like this:

case class Flatten(aFirst: String, aSecond: String, bValue: String)

But I would like to avoid to build my own constructor (or create a function manually) like this:

object Flatten {

  def apply(nested: Nested): Flatten = {
    Flatten(nested.a.first, nested.a.second, nested.b.value)
  }
}

Note: in real use case, case classes are more complex and I would like to use the method several times on different case classes.

Upvotes: 5

Views: 1801

Answers (1)

dyrkin
dyrkin

Reputation: 544

You can play with reflections api assuming that the target case class field names have a predefined format. Take a look at the example

import scala.reflect.runtime.universe._

class Converter(any: Any) {
  private val rm = runtimeMirror(any.getClass.getClassLoader)

  private def nameToPath(name: String, pathElem: String = "", pathElems: List[String] = List()): List[String] =
    if (name.isEmpty) pathElems :+ pathElem.toLowerCase()
    else if (name.head.isUpper) nameToPath(name.tail, name.head.toString, pathElems :+ pathElem)
    else nameToPath(name.tail, pathElem + name.head, pathElems)

  private def valueByPath(v: Any, pathElems: List[String]): Any =
    if (pathElems.isEmpty) v
    else {
      val im = rm.reflect(v)
      val fieldName = TermName(pathElems.head)
      val field = im.symbol.info.member(fieldName).asTerm
      val value = im.reflectField(field).get
      valueByPath(value, pathElems.tail)
    }

  def convertTo[T: TypeTag]: T = {
    val target = typeOf[T]
    val fieldNames = target.decls.sorted.collect {
      case m: MethodSymbol if m.isCaseAccessor => m
    }

    val paths = fieldNames.map(s => nameToPath(s.name.toString))
    val values = paths.map(valueByPath(any, _))

    val constructorSymbol = target.decl(termNames.CONSTRUCTOR)

    val defaultConstructor = constructorSymbol match {
      case cs: MethodSymbol => cs
      case ts: TermSymbol =>
        ts.alternatives.collectFirst {
          case ms: MethodSymbol if ms.isPrimaryConstructor => ms
        }.get
    }

    val cs = target.typeSymbol.asClass
    val cm = rm.reflectClass(cs)
    val constructor = cm.reflectConstructor(defaultConstructor)
    constructor(values: _*).asInstanceOf[T]
  }
}

implicit class AnyOps(any: Any) {
  def to[T: TypeTag]: T = new Converter(any).convertTo[T]
}

using

val a = A("1", "2")
val b = B("3")
val n = Nested(a, b)

val r = n.to[Flatten]

output

r: Flatten = Flatten(1,2,3)

Upvotes: 2

Related Questions