Important note
This answer is not a good one in my opinion (I posted it on OP's request). It involves complicated structures from shapeless library to avoid dealing with macros or reflection (actually, it uses macros under the hood, but shapeless allows to forget about them).
This is based on shapeless Generic macros. It involves creating a typeclass for your data type, and infering the instances of this typeclass for any type which has a LabelledGeneric in shapeless, ie a data structure with sealed traits and case classes:
import shapeless.{:+:, CNil, Coproduct, HList, HNil, LabelledTypeClass, LabelledTypeClassCompanion}
trait RecursiveFields[T] {
def fields: List[List[String]]
override def toString = fields.map(_.mkString(".")).mkString("(", ", ", ")")
}
object RecursiveFields extends LabelledTypeClassCompanion[RecursiveFields] {
def apply[T](f: List[List[String]]): RecursiveFields[T] = new RecursiveFields[T] {
val fields = f
}
implicit val string: RecursiveFields[String] = apply[String](Nil)
implicit def anyVal[T <: AnyVal]: RecursiveFields[T] = apply[T](Nil)
object typeClass extends LabelledTypeClass[RecursiveFields] {
override def product[H, T <: HList](name: String, ch: RecursiveFields[H], ct: RecursiveFields[T]): RecursiveFields[shapeless.::[H, T]] =
RecursiveFields{
val hFields = if (ch.fields.isEmpty) List(List(name)) else ch.fields.map(name :: _)
hFields ++ ct.fields
}
override def emptyProduct: RecursiveFields[HNil] = RecursiveFields(Nil)
override def project[F, G](instance: => RecursiveFields[G], to: (F) => G, from: (G) => F): RecursiveFields[F] =
RecursiveFields(instance.fields)
override def coproduct[L, R <: Coproduct](name: String, cl: => RecursiveFields[L], cr: => RecursiveFields[R]): RecursiveFields[:+:[L, R]] =
RecursiveFields[L :+: R](product(name, cl, emptyProduct).fields)
override def emptyCoproduct: RecursiveFields[CNil] = RecursiveFields(Nil)
}
}
Note that the coproduct part is not necessary while you only deal with case classes, (you may replace LabelledTypeClass with LabelledProductTypeClass, and the same for Companion). However, since Option is a coproduct, this is not true in our case, but it is unclear what choice we should make in that case (I chose to take the first possible choice in the coproduct, but this is not satisfactory).
To use this, just call implicitly[RecursiveFields[A]].fields to get a list whose elements are the desired fields (splitted on ., so that b.name is actually kept as List(b, name)).