Reputation: 1219
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
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