dirceusemighini
dirceusemighini

Reputation: 1354

How to get list of fields and subfields of a Scala case class

I have a nested data structure in case classes, like

Update2 All vals are optional

case class A(b:Option[B] = None,c:Option[C] = None,d:Option[D] = None)
case class B(id:Option[String] = None, name:Option[String] = None)
case class C(cNode:Option[String] = None, cuser:Option[String] = None)
case class D(dData:Option[String] = None, dField:Option[String] = None)

I'm looking for a regex to track all fields from the Class A, through all of it's subclasses.

The code from this answer solve the first step of my problem. It list all of the fields of the first level (class A). I tried to change it to recursive call the same method but I can't get the TypeTag information of a MethodSymbol.

The result I'm expecting for this is a method that receives A as argument, and returns

(b.id, b.name,c.cNode, c.cUser,d.dData, d.dFile)

How can I get the subfields attribute names from a case class?

UPDATE

I'm using scala 2.11

I also want it to be generated by reflection/macros because the data structure is complex and I want it to be updated when the case class are updated.

Upvotes: 1

Views: 2667

Answers (2)

Jasper-M
Jasper-M

Reputation: 15086

You can call methodSymbol.returnType. It will give you the return type of the case accessor on which you can then recursively collect all its case accessors.

Here is a full example (assuming that every field is an Option):

scala> :paste
// Entering paste mode (ctrl-D to finish)

case class A(b:Option[B] = None,c:Option[C] = None,d:Option[D] = None)
case class B(id:Option[String] = None, name:Option[String] = None)
case class C(cNode:Option[String] = None, cuser:Option[String] = None)
case class D(dData:Option[String] = None, dField:Option[String] = None)

import scala.reflect.runtime.universe._

def allFields[T: TypeTag]: List[String] = {
  def rec(tpe: Type): List[List[Name]] = { 
    val collected = tpe.members.collect {
      case m: MethodSymbol if m.isCaseAccessor => m
    }.toList
    if (collected.nonEmpty)
      collected.flatMap(m => rec(m.returnType.typeArgs.head).map(m.name :: _))
    else
      List(Nil)
  }
  rec(typeOf[T]).map(_.mkString("."))
}

// Exiting paste mode, now interpreting.


scala> allFields[A]
res0: List[String] = List(d.dField, d.dData, c.cuser, c.cNode, b.name, b.id)

Upvotes: 4

Cyrille Corpet
Cyrille Corpet

Reputation: 5315

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)).

Upvotes: 0

Related Questions