linehrr
linehrr

Reputation: 1748

scala cast object based on reflection symbol

I have a Scala reflection Symbol.

val symbol = scala.reflect.api.Symbol

how can I cast an object

val obj: Any

to the type of that Symbol ?

reason why I am asking is that I have a implicit method to convert Map[String, Any] to a Scala case class, but in side I want to to cast on each field.

TODO part is the place I want to cast the type.

object MapStringAnyOps {
  implicit class MapStringAnyOps(m: Map[String, _]) {
    def toCaseClass[T: TypeTag : ClassTag]: T = {
      val rm = runtimeMirror(classTag[T].runtimeClass.getClassLoader)
      val destType = typeOf[T]
      val classTest = destType.typeSymbol.asClass
      val classMirror = rm.reflectClass(classTest)
      val constructor = destType.decl(termNames.CONSTRUCTOR).asMethod
      val constructorMirror = classMirror.reflectConstructor(constructor)

      val constructorArgs = constructor.paramLists.flatten.map((param: Symbol) => {
        val paramName = param.name.toString
        if (param.typeSignature <:< typeOf[Option[Any]])
          m.get(paramName)  **// TODO in here, I can to cast this value of Any into the proper type of this field, but I only have Symbol so I can't simply use asInstanceOf[T]**
        else
          m.getOrElse(paramName, throw new IllegalArgumentException("Map is missing required parameter named " + paramName))
      })
      try {
        constructorMirror(constructorArgs: _*).asInstanceOf[T]
      } catch {
        case e: java.lang.IllegalArgumentException =>
          throw new java.lang.IllegalArgumentException(s"Error mapping to class constructor:\n Casting from row: ${constructorArgs.map(r => r.getClass.getName).mkString(",")}, to constructor: ${constructor.paramLists.flatten.map(r => r.typeSignature.toString).mkString(",")}", e)
      }
    }
  }
}

Upvotes: 0

Views: 255

Answers (1)

Dmytro Mitin
Dmytro Mitin

Reputation: 51703

You can't cast.

In order to cast .asInstanceOf[T] you need to know T at compile time. But you know the symbol at runtime. You can do something later (e.g. at runtime) based on the information you get earlier (e.g. at compile time) if you persist somehow the information. But you can't cast earlier (i.e. at compile time) based on the information you get later (i.e. at runtime).

But actually you don't need to cast. constructorMirror accepts Any*.


Just in case, you could cast if you knew the type at compile time, e.g. if you used compile-time reflection (i.e. macros) rather than runtime reflection

import scala.language.experimental.macros
import scala.reflect.macros.whitebox

trait ToCaseClass[T] {
  def apply(m: Map[String, _]): T
}
object ToCaseClass {
  def apply[T: ToCaseClass]: ToCaseClass[T] = implicitly

  implicit def mkToCaseClass[T]: ToCaseClass[T] = macro mkToCaseClassImpl[T]

  def mkToCaseClassImpl[T: c.WeakTypeTag](c: whitebox.Context): c.Tree = {
    import c.universe._
    val T = weakTypeOf[T]
    val constructor = T.decl(termNames.CONSTRUCTOR).asMethod
    val constructorArgs = constructor.paramLists.flatten.map((param: Symbol) => {
      val paramName = param.name.toString
      val paramType = param.typeSignature
      q"m($paramName).asInstanceOf[$paramType]"
    })
    q"""
      new ToCaseClass[$T] {
        override def apply(m: Map[String, _]): $T = new $T(..$constructorArgs)
      }
    """
  }
}

object MapStringAnyOps {
  implicit class MapStringAnyOps(m: Map[String, _]) {
    def toCaseClass[T: ToCaseClass]: T = ToCaseClass[T].apply(m)
  }
}
case class A(i: Int, s: String)

import MapStringAnyOps._
Map("s" -> "a", "i" -> 1).toCaseClass[A] // A(1,a)

By the way, you can convert a Map to a case class and back using libraries

import shapeless.{HList, LabelledGeneric}
import shapeless.ops.maps.FromMap
import shapeless.ops.product.ToMap

def fromMap[T]: PartiallyApplied[T] = new PartiallyApplied[T]

class PartiallyApplied[T] {
  def apply[R <: HList](m: Map[Symbol, _])(implicit
    gen: LabelledGeneric.Aux[T, R],
    fromMap: FromMap[R]
  ): Option[T] = fromMap(m).map(gen.from)
}

fromMap[A].apply(Map("s" -> "a", "i" -> 1)
  .map { case (k, v) => Symbol(k) -> v }) // converting Map[String, _] into Map[Symbol, _]
  .get
// A(1,a)

ToMap[A].apply(A(1, "a"))
  .map { case (k, v) => k.name -> v } // converting Map[Symbol, _] into Map[String, _]
// Map(s -> a, i -> 1)

In Shapeless there is a type class ToMap converting a case class to a Map but the type class FromMap converts a Map into HList rather than case class, that's why we need fromMap method.

How to use shapeless to convert generic Map[String, Any] to case class inside generic function?

Shapeless code to convert Map[String, Any] to case class cannot handle optional substructures

Converting Map[String,Any] to a case class using Shapeless

How to convert generic potentially nested map Map[String, Any] to case class using any library in Scala?

Scala macros for nested case classes to Map and other way around

Converting nested case classes to nested Maps using Shapeless

Convert Nested Case Classes to Nested Maps in Scala

Case class to map in Scala

Scala: convert map to case class

Upvotes: 1

Related Questions