dimanche 29 mars 2020

Instantiating Scala class/case class via reflection

Description

I'm trying to build a tool capable of converting a Map[String, Any] into a class/case class instance. If the class definition contains default parameters which are not specified in the Map, then the default values would apply.

The following code snippet allows retrieving the primary class constructor:

import scala.reflect.runtime.universe._

def constructorOf[A: TypeTag]: MethodMirror = {
  val constructor = typeOf[A]
    .decl(termNames.CONSTRUCTOR)
    // Getting all the constructors
    .alternatives
    .map(_.asMethod)
    // Picking the primary constructor
    .find(_.isPrimaryConstructor)
    // A class must always have a primary constructor, so this is safe
    .get
  typeTag[A].mirror
    .reflectClass(typeOf[A].typeSymbol.asClass)
    .reflectConstructor(constructor)
}

Given the following simple class definition:

class Foo(a: String = "foo") {
  override def toString: String = s"Foo($a)"
}

I can easily create a new Foo instance when providing both arguments:

val bar = constructorOf[Foo].apply("bar").asInstanceOf[Foo]
bar: Foo = Foo(bar)

The problem arises when attempting to create an instance without specifying the constructor parameter (which should still work, as parameter a has a default value):

val foo = constructorOf[Foo].apply()
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 0
    at scala.collection.mutable.WrappedArray$ofRef.apply(WrappedArray.scala:127)
    at scala.reflect.runtime.JavaMirrors$JavaMirror$JavaVanillaMethodMirror2.jinvokeraw(JavaMirrors.scala:384)
    at scala.reflect.runtime.JavaMirrors$JavaMirror$JavaMethodMirror.jinvoke(JavaMirrors.scala:339)
    at scala.reflect.runtime.JavaMirrors$JavaMirror$JavaVanillaMethodMirror.apply(JavaMirrors.scala:355)

Already tried

I've already seen similar questions like this, and tried this one, which didn't work for me:

Exception in thread "main" java.lang.NoSuchMethodException: Foo.$lessinit$greater$default$1()
    at java.lang.Class.getMethod(Class.java:1786)
    at com.dhermida.scala.jdbc.Test$.extractDefaultConstructorParamValue(Test.scala:16)
    at com.dhermida.scala.jdbc.Test$.main(Test.scala:10)
    at com.dhermida.scala.jdbc.Test.main(Test.scala)

Goal

I would like to invoke a class' primary constructor, using the default constructor values in case these are not set in the input Map. My initial approach consists of:

  1. [Done] Retrieve the class' primary constructor.
  2. [Done] Identify which constructor argument(s) have a default parameter.
  3. Call constructorOf[A].apply(args: Any*) constructor, using the default values for any argument not present in the input MapNot working.

Is there any way to retrieve the primary constructor's default argument values using Scala Reflection API?





Aucun commentaire:

Enregistrer un commentaire