Scala 2.10:动态实例化 case 类

3
我有一个“动态实例化case class”的梦想 - 并根据每个字段的类型提供一些虚拟数据(我将稍后创建一些规则)。到目前为止,我编写了一些可以使用StringLongInt的case class的代码...但如果它能够处理嵌套的case class会更好。因此,我可以实例化case class RequiredAPIResponse (stringValue: String, longValue: Long, intValue: Int),但无法实例化外部类;其中外部类是...
case class Inner (deep: String)
case class Outer (in : Inner)

代码是

 def fill[T <: Object]()(implicit mf: ClassTag[T]) : T = {

      val declaredConstructors = mf.runtimeClass.getDeclaredConstructors
      if (declaredConstructors.length != 1)
      Logger.error(/*T.toString + */" has " + declaredConstructors.length + " constructors --- only 1 currently supported.")
      val constructor = declaredConstructors.headOption.get

      val m = constructor.getParameterTypes.map(p => {
          Logger.info("getName " + p.getName +" --- getCanonicalName " + p.getCanonicalName)
          Logger.info(p.getCanonicalName)

        p.getCanonicalName match {
          case "java.lang.String" => /*"Name"->*/ val s : java.lang.String = "DEFAULT STRING"
            s
          case "long" => /*"Name"-> */ val l : java.lang.Long = new java.lang.Long(99)
            l
          case "int" => /*"Name"->*/ val i : java.lang.Integer = new java.lang.Integer(99)
            i
          case _ => /*"Name"->*/

            So around here I am stuck!
        //THIS IS MADE UP :) But I want to get the "Type" and recursively call fill     
            //fill[p # Type] <- not real scala code

            //I can get it to work in a hard coded manner
            //fill[Inner]

        }
      })

我觉得在Scala: How to invoke method with type parameter and manifest without knowing the type at compile time?的最后一个回答是一个答案的起点。因此,fill应该采用ClassTag或TypeTag,而不是使用T <:Object?
这段代码起源于How can I transform a Map to a case class in Scala?,其中提到(就像Lift-Framework一样)我确实有liftweb源代码;但是到目前为止,我还没有成功地解开它的所有秘密。
编辑 --- 基于Imm的观点,我已经使下面的代码工作了(对他的答案进行了一些小更新)。
def fillInner(cls: Class[_]) : Object = {
    val declaredConstructors = cls.getDeclaredConstructors
    if (declaredConstructors.length != 1)
      Logger.error(/*T.toString + */ " has " + declaredConstructors.length + " constructors --- only 1 currently supported.")
    val constructor = declaredConstructors.headOption.get

    val m = constructor.getParameterTypes.map(p => {
      Logger.info("getName " + p.getName + " --- getCanonicalName " + p.getCanonicalName)
      Logger.info(p.getCanonicalName)

      p.getCanonicalName match {
        case "java.lang.String" => /*"Name"->*/ val s: java.lang.String = "DEFAULT STRING"
          s
        case "long" => /*"Name"-> */ val l: java.lang.Long = new java.lang.Long(99)
          l
        case "int" => /*"Name"->*/ val i: java.lang.Integer = new java.lang.Integer(99)
          i
        case _ => fillInner(p)
      }

    })

    constructor.newInstance(m: _*).asInstanceOf[Object]

  }

    def fill[T](implicit mf: ClassTag[T]) : T = fillInner(mf.runtimeClass).asInstanceOf[T]

感谢您,Brent。

可能ScalaTest或ScalaCheck已经有类似自动生成测试用例的功能了。Shapeless肯定是一个选择(从答案中看来,Kiama也是):https://dev59.com/5mrXa4cB1Zd3GeqPD-eU - Gábor Bakos
2个回答

2

您实际上并未使用ClassTag,只是使用了Class [_],而且所有这些都不是类型安全的(这只是 Java 反射),因此请递归传递Class[_]

def fillInner(cls: Class[_]) : Any = {
  val declaredConstructors = cls.getDeclaredConstructors
  if (declaredConstructors.length != 1)
  Logger.error(/*T.toString + */" has " + declaredConstructors.length + " constructors --- only 1 currently supported.")
  val constructor = declaredConstructors.headOption.get

  val m = constructor.getParameterTypes.map(p => {
      Logger.info("getName " + p.getName +" --- getCanonicalName " + p.getCanonicalName)
      Logger.info(p.getCanonicalName)

    p.getCanonicalName match {
      case "java.lang.String" => /*"Name"->*/ val s : java.lang.String = "DEFAULT STRING"
        s
      case "long" => /*"Name"-> */ val l : java.lang.Long = new java.lang.Long(99)
        l
      case "int" => /*"Name"->*/ val i : java.lang.Integer = new java.lang.Integer(99)
        i
      case _ => fillInner(p)
    }
  })

def fill[T: ClassTag]: T = fillInner(classOf[T].runtimeClass).asInstanceOf[T]

但是您可能可以通过使用 Shapeless 以类型安全的方式实现您想要做的事情:

trait Supplier[T] {
  def supply: T
}
object Supplier[T] {
  implicit val intSupplier = new Supplier[Int] {
    def supply = 99
  }
  implicit val stringSupplier = ...
  implicit val emptyHListSupplier = new Supplier[HNil] {
    def supply = HNil
  }
  implicit def consHListSupplier[H, T <: HList](
    implicit headSupplier: Supplier[H], 
      tailSupplier: Supplier[T]) = new Supplier[H :: T] {
    def supply = headSupplier.supply :: tailSupplier.supply
   }
}

然后通过隐式解析的魔法,您可以获得任何递归HList的Supplier[(String :: HNil) :: Int :: HNil],只要它最终仅包含您已经拥有供应商的值;您只需要更多的shapeless(在版本1或2中不同,并且我已经做了一段时间,所以我不记得具体情况),以在这些之间进行转换和案例类。

太棒了,谢谢你的建议,关于在fillInner中不需要使用ClassTag的观点帮助了我!等我的大脑不再感到混乱后,我会研究一下shapeless。 - brent

0

如果你只是在测试中使用它,最好的方法是使用Scala/Java反射。

与使用宏相比的优点是编译速度更快。与使用scalacheck相关库相比的优点是API更好。

设置它有点复杂。这里是一个完整的工作代码,你可以将其复制到你的代码库中:

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")
  }
}

而且,当你想要使用它时,可以调用:

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

上面的代码生成任意且唯一的值。它们并不完全是随机的。该API非常好用。考虑到它使用反射,因此在测试中使用最佳。
阅读我们的完整博客文章:https://give.engineering/2018/08/24/instantiate-case-class-with-arbitrary-value.html

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接