scalareflectionscala-3

Scala 3 compile time mirrors: how to get field names when I don't have an instance of the type


I have defined this in order to serialize any case class whose fields a scalars:

import scala.compiletime.*
import scala.deriving.*

trait Scalar[T]:
  def convert(t: T): AnyRef
  val sqlType: String

given Scalar[String] with
   def convert(t: String) = t
   val sqlType = "VARCHAR"

given Scalar[Int] with
   def convert(t: Int) = Integer.valueOf(t)
   val sqlType = "INTEGER"

trait Serializable[T]:
   def serialize(v: T): Map[String, AnyRef]
   def fields(v: T): Map[String, String]

object Serializable:
    inline def summonAll[T <: Tuple]: List[Scalar[?]] =
        inline erasedValue[T] match
            case _: EmptyTuple => Nil
            case _: (t *: ts) => summonInline[Scalar[t]] :: summonAll[ts]

    inline given derived[A <: Product](using m: Mirror.ProductOf[A]): Serializable[A] =
        val scalars = summonAll[m.MirroredElemTypes]
        new Serializable[A]:
            override def serialize(v: A): Map[String, AnyRef] =
                val pro = v.asInstanceOf[Product]
                pro.productElementNames.zip(pro.productIterator).zip(scalars).map {
                    case ((name, value), scalar) => 
                        name -> scalar.asInstanceOf[Scalar[Any]].convert(value)
                }.toMap
            override def fields(v: A): Map[String, String] =
                val pro = v.asInstanceOf[Product]
                pro.productElementNames.zip(scalars).map {
                    case (name, scalar) => 
                        name -> scalar.asInstanceOf[Scalar[Any]].sqlType
                }.toMap

case class Person(name: String, age: Int) derives Serializable

And it works thanks to the magic compile time mirrors.

But I need to define this:

trait Serializable[T]:
   def serialize(v: T): Map[String, AnyRef]
   val fields: Map[String, String]

instead of:

   def fields(v: T): Map[String, String]

because the fields depend not on the instance, but on the class type.

The problem is that I don't know how to get field names, when I don't have an instace of the case class.


Solution

  • Here is the solution I've found, maybe it's no the best but it works:

    #!/usr/bin/env -S scala-cli -j system
    import scala.compiletime.*
    import scala.deriving.*
    
    trait Scalar[T]:
      def convert(t: T): AnyRef
      val sqlType: String
    
    given Scalar[String] with
       def convert(t: String) = t
       val sqlType = "VARCHAR"
    
    given Scalar[Int] with
       def convert(t: Int) = Integer.valueOf(t)
       val sqlType = "INTEGER"
    
    trait Serializable[T]:
       def serialize(v: T): Map[String, AnyRef]
       val fields: Map[String, String]
    
    object Serializable:
        inline def summonAll[T <: Tuple]: List[Scalar[?]] =
            inline erasedValue[T] match
                case _: EmptyTuple => Nil
                case _: (t *: ts) => summonInline[Scalar[t]] :: summonAll[ts]
    
        inline given derived[A <: Product](using m: Mirror.ProductOf[A]): Serializable[A] =
            val scalars = summonAll[m.MirroredElemTypes]
            new Serializable[A]:
                override def serialize(v: A): Map[String, AnyRef] =
                    val pro = v.asInstanceOf[Product]
                    pro.productElementNames.zip(pro.productIterator).zip(scalars).map {
                        case ((name, value), scalar) => 
                            name -> scalar.asInstanceOf[Scalar[Any]].convert(value)
                    }.toMap
                override val fields: Map[String, String] =
                    import scala.compiletime.constValueTuple
                    val nombres = constValueTuple[m.MirroredElemLabels].productIterator
                    nombres.zip(scalars).map {
                        case (name, scalar) => 
                            name.toString() -> scalar.asInstanceOf[Scalar[Any]].sqlType
                    }.toMap
    
    case class Person(name: String, age: Int) derives Serializable
    val sp = summon[Serializable[Person]]
    assert(sp.fields("age") == "INTEGER")