Instantiating a case class with default args via reflection

后端 未结 4 1472
孤街浪徒
孤街浪徒 2021-01-24 04:03

I need to be able to instantiate various case classes through reflection, both by figuring out the argument types of the constructor, as well as invoking the constructor with al

相关标签:
4条回答
  • 2021-01-24 04:28

    So in the linked question, the :power REPL uses internal API, which means that defaultGetterName is not available, so we need to construct that from hand. An adoption from @som-snytt 's answer:

    def newDefault[A](implicit t: reflect.ClassTag[A]): A = {
      import reflect.runtime.{universe => ru, currentMirror => cm}
    
      val clazz  = cm.classSymbol(t.runtimeClass)
      val mod    = clazz.companionSymbol.asModule
      val im     = cm.reflect(cm.reflectModule(mod).instance)
      val ts     = im.symbol.typeSignature
      val mApply = ts.member(ru.newTermName("apply")).asMethod
      val syms   = mApply.paramss.flatten
      val args   = syms.zipWithIndex.map { case (p, i) =>
        val mDef = ts.member(ru.newTermName(s"apply$$default$$${i+1}")).asMethod
        im.reflectMethod(mDef)()
      }
      im.reflectMethod(mApply)(args: _*).asInstanceOf[A]
    }
    
    case class Foo(bar: Int = 33)
    
    val f = newDefault[Foo]  // ok
    

    Is this really the shortest path?

    0 讨论(0)
  • 2021-01-24 04:30

    Not minimized... and not endorsing...

    scala> import scala.reflect.runtime.universe
    import scala.reflect.runtime.universe
    
    scala> import scala.reflect.internal.{ Definitions, SymbolTable, StdNames }
    import scala.reflect.internal.{Definitions, SymbolTable, StdNames}
    
    scala> val ds = universe.asInstanceOf[Definitions with SymbolTable with StdNames]
    ds: scala.reflect.internal.Definitions with scala.reflect.internal.SymbolTable with scala.reflect.internal.StdNames = scala.reflect.runtime.JavaUniverse@52a16a10
    
    scala> val n = ds.newTermName("foo")
    n: ds.TermName = foo
    
    scala> ds.nme.defaultGetterName(n,1)
    res1: ds.TermName = foo$default$1
    
    0 讨论(0)
  • 2021-01-24 04:46

    Here's a working version that you can copy into your codebase:

    import scala.reflect.api
    import scala.reflect.api.{TypeCreator, Universe}
    import scala.reflect.runtime.universe._
    
    object Maker {
      val mirror = runtimeMirror(getClass.getClassLoader)
    
      var makerRunNumber = 1
    
      def apply[T: TypeTag]: T = {
        val method = typeOf[T].companion.decl(TermName("apply")).asMethod
        val params = method.paramLists.head
        val args = params.map { param =>
          makerRunNumber += 1
          param.info match {
            case t if t <:< typeOf[Enumeration#Value] => chooseEnumValue(convert(t).asInstanceOf[TypeTag[_ <: Enumeration]])
            case t if t =:= typeOf[Int] => makerRunNumber
            case t if t =:= typeOf[Long] => makerRunNumber
            case t if t =:= typeOf[Date] => new Date(Time.now.inMillis)
            case t if t <:< typeOf[Option[_]] => None
            case t if t =:= typeOf[String] && param.name.decodedName.toString.toLowerCase.contains("email") => s"random-$arbitrary@give.asia"
            case t if t =:= typeOf[String] => s"arbitrary-$makerRunNumber"
            case t if t =:= typeOf[Boolean] => false
            case t if t <:< typeOf[Seq[_]] => List.empty
            case t if t <:< typeOf[Map[_, _]] => Map.empty
            // Add more special cases here.
            case t if isCaseClass(t) => apply(convert(t))
            case t => throw new Exception(s"Maker doesn't support generating $t")
          }
        }
    
        val obj = mirror.reflectModule(typeOf[T].typeSymbol.companion.asModule).instance
        mirror.reflect(obj).reflectMethod(method)(args:_*).asInstanceOf[T]
      }
    
      def chooseEnumValue[E <: Enumeration: TypeTag]: E#Value = {
        val parentType = typeOf[E].asInstanceOf[TypeRef].pre
        val valuesMethod = parentType.baseType(typeOf[Enumeration].typeSymbol).decl(TermName("values")).asMethod
        val obj = mirror.reflectModule(parentType.termSymbol.asModule).instance
    
        mirror.reflect(obj).reflectMethod(valuesMethod)().asInstanceOf[E#ValueSet].head
      }
    
      def convert(tpe: Type): TypeTag[_] = {
        TypeTag.apply(
          runtimeMirror(getClass.getClassLoader),
          new TypeCreator {
            override def apply[U <: Universe with Singleton](m: api.Mirror[U]) = {
              tpe.asInstanceOf[U # Type]
            }
          }
        )
      }
    
      def isCaseClass(t: Type) = {
        t.companion.decls.exists(_.name.decodedName.toString == "apply") &&
          t.decls.exists(_.name.decodedName.toString == "copy")
      }
    }
    

    And, when you want to use it, you can call:

    val user = Maker[User]
    val user2 = Maker[User].copy(email = "someemail@email.com")
    

    The code above generates arbitrary and unique values. The data aren't exactly randomised. It's best for using in tests.

    It works with Enum and nested case class. You can also easily extend it to support some other special types.

    Read our full blog post here: https://give.engineering/2018/08/24/instantiate-case-class-with-arbitrary-value.html

    0 讨论(0)
  • 2021-01-24 04:49

    This is the most complete example how to create case class via reflection with default constructor parameters(Github source):

    import scala.reflect.runtime.universe
    import scala.reflect.internal.{Definitions, SymbolTable, StdNames}
    
    object Main {
      def newInstanceWithDefaultParameters(className: String): Any = {
        val runtimeMirror: universe.Mirror = universe.runtimeMirror(getClass.getClassLoader)
        val ds = universe.asInstanceOf[Definitions with SymbolTable with StdNames]
        val classSymbol = runtimeMirror.staticClass(className)
        val classMirror = runtimeMirror.reflectClass(classSymbol)
        val moduleSymbol = runtimeMirror.staticModule(className)
        val moduleMirror = runtimeMirror.reflectModule(moduleSymbol)
        val moduleInstanceMirror = runtimeMirror.reflect(moduleMirror.instance)
        val defaultValueMethodSymbols = moduleMirror.symbol.info.members
          .filter(_.name.toString.startsWith(ds.nme.defaultGetterName(ds.newTermName("apply"), 1).toString.dropRight(1)))
          .toSeq
          .reverse
          .map(_.asMethod)
        val defaultValueMethods = defaultValueMethodSymbols.map(moduleInstanceMirror.reflectMethod).toList
        val primaryConstructorMirror = classMirror.reflectConstructor(classSymbol.primaryConstructor.asMethod)
        primaryConstructorMirror.apply(defaultValueMethods.map(_.apply()): _*)
      }
    
      def main(args: Array[String]): Unit = {
        val instance = newInstanceWithDefaultParameters(classOf[Bar].getName)
        println(instance)
      }
    }
    
    case class Bar(i: Int = 33)
    
    0 讨论(0)
提交回复
热议问题