Troubles with getting case class fields via reflection

266 views Asked by At

I have a scala code: trait model and its implementation case class Category with custom annotation which i wish to read later

import scala.annotation.StaticAnnotation
class ExtraFields() extends StaticAnnotation

trait Model {}

case class MyCategory( id: Option[Int],
                     name: String,
                     level: Option[Int],

                     @ExtraFields
                     parent: Option[MyCategory] = None
                   ) extends Model {

}

and i have trait DBModel and its implementation MyCategoryModel. And i pass MyCategory as type parameter to DBModel

trait DBModel[T <: Model] {
  lazy val fields = getClassFields[T]
  lazy val fields2 = getClassFields2[T]
}

object MyCategoryModel extends DBModel[MyCategory] {
  def getFields = fields
  def getFields2 = fields2
}

i want trait DBModel to read fields of case class passed as T parameter, so i call two functions

def getClassFields[T] = {
  symbolOf[T].asClass.primaryConstructor
    .typeSignature.paramLists.head.map {v =>
      v.name.toString -> v.annotations
    }
}

def getClassFields2[T: TypeTag]: Iterable[(String, List[universe.Annotation])] =
  typeOf[T].members.collect {
    case m: MethodSymbol if m.isCaseAccessor =>
      m.name.toString -> m.annotations
  }

but none of them is working

val res = MyCategoryModel.getFields // runtime exception: free type T is not a class
val res2 = MyCategoryModel.getFields2 // compile error: No TypeTag available for T

If i call them directly

getClassFields[MyCategory]
getClassFields2[MyCategory]

for a first function i get the same error, for the second i get result but universe.annotations can not see my ExtraFields annotation, returning empty lists for each of fields

Could you please explain this black magic and how to conqure it in my case Thank you

UPD playground https://scastie.scala-lang.org/DTTTyA0CSTSZ0Vj7KFW6Hw

1

There are 1 answers

4
Ivan Stanislavciuc On BEST ANSWER

You're missing type class WeakTypeTag for getClassFields.

Additionally, you're not passing the type classes in a trait DBModel[T <: Model].

One solution is to define them as context bounds of type T. But for this to work, it is required to pass these type classes via a constructor. And because traits do not have constructors, the solution is to use abstract class.

So the following fixes the compilation problems

abstract class DBModel[T <: Model : WeakTypeTag : TypeTag] {
  lazy val fields = getClassFields[T]
  lazy val fields2 = getClassFields2[T]
}

def getClassFields[T: WeakTypeTag] = ???
def getClassFields2[T: TypeTag] = ???

The method getClassFields2 is not finding annotations because of the way annotations are specified. If defined as following, both methods work. For more info read meta docs

  
import scala.annotation.meta.{getter, param}
case class MyCategory(
     id: Option[Int],
     name: String,
     level: Option[Int],
     @(ExtraFields @param @getter) parent: Option[MyCategory] = None
  ) extends Model