Automatic display of case class

I create a web application using Play and Slick and find myself in a situation where the forms facing the user are similar, but not exactly the same as the database model.

Therefore, I have two very similar case classes and I need to map them to each other (for example, when filling out a form to visualize the "update" view).

In the case when I'm interested, the database case model class is a super-set of case-class forms, i.e. the only difference between the two is that the database model has two more fields (basically two identifiers).

Now I'm wondering if there will be a way to create a small library (for example, macro-managed) to automatically populate the case class of the form from the database case class based on the names of the participants. I have seen that you can access this information by reflection with Paranamer, but I would rather not risk it.

+2
source share
1 answer

Here is a solution using Dynamic, because I wanted to try. The macro would statically decide whether to emit the application of the source value method, the default method, or simply to feed a literal. The syntax may look something like this newFrom[C](k). (Update: see below macro.)

import scala.language.dynamics
trait Invocable extends Dynamic {
  import scala.reflect.runtime.currentMirror
  import scala.reflect.runtime.universe._

  def applyDynamic(method: String)(source: Any) = {
    require(method endsWith "From")
    def caseMethod(s: Symbol) = s.asTerm.isCaseAccessor && s.asTerm.isMethod
    val sm = currentMirror reflect source
    val ms = sm.symbol.asClass.typeSignature.members filter caseMethod map (_.asMethod)
    val values = ms map (m => (m.name, (sm reflectMethod m)()))
    val im = currentMirror reflect this
    invokeWith(im, method dropRight 4, values.toMap)
  }

  def invokeWith(im: InstanceMirror, name: String, values: Map[Name, Any]): Any = {
    val at = TermName(name)
    val ts = im.symbol.typeSignature
    val method = (ts member at).asMethod

    // supplied value or defarg or default val for type of p
    def valueFor(p: Symbol, i: Int): Any = {
      if (values contains p.name) values(p.name)
      else ts member TermName(s"$name$$default$$${i+1}") match {
        case NoSymbol =>
          if (p.typeSignature.typeSymbol.asClass.isPrimitive) {
            if (p.typeSignature <:< typeOf[Int]) 0
            else if (p.typeSignature <:< typeOf[Double]) 0.0
            else ???
          } else null
        case defarg   => (im reflectMethod defarg.asMethod)()
      }
    }
    val args = (for (ps <- method.paramss; p <- ps) yield p).zipWithIndex map (p => valueFor(p._1,p._2))
    (im reflectMethod method)(args: _*)
  }
}
case class C(a: String, b: Int, c: Double = 2.0, d: Double)
case class K(b: Int, e: String, a: String)
object C extends Invocable
object Test extends App {
  val res = C applyFrom K(8, "oh", "kay")
  Console println res      // C(kay,8,2.0,0.0)
}

Update: here is a macro version, more for fun than for profit:

import scala.language.experimental.macros
import scala.reflect.macros._
import scala.collection.mutable.ListBuffer

def newFrom[A, B](source: A): B = macro newFrom_[A, B]

def newFrom_[A: c.WeakTypeTag, B: c.WeakTypeTag](c: Context)(source: c.Expr[A]): c.Expr[B] = { 
  import c.{ literal, literalNull } 
  import c.universe._
  import treeBuild._
  import nme.{ CONSTRUCTOR => Ctor } 

  def caseMethod(s: Symbol) = s.asTerm.isCaseAccessor && s.asTerm.isMethod
  def defaulter(name: Name, i: Int): String = s"${name.encoded}$$default$$${i+1}"
  val noargs = List[c.Tree]()

  // side effects: first evaluate the arg
  val side = ListBuffer[c.Tree]()
  val src = TermName(c freshName "src$")
  side += ValDef(Modifiers(), src, TypeTree(source.tree.tpe), source.tree)

  // take the arg as instance of a case class and use the case members
  val a = implicitly[c.WeakTypeTag[A]].tpe
  val srcs = (a.members filter caseMethod map (m => (m.name, m.asMethod))).toMap

  // construct the target, using src fields, defaults (from the companion), or zero
  val b = implicitly[c.WeakTypeTag[B]].tpe
  val bm = b.typeSymbol.asClass.companionSymbol.asModule
  val bc = bm.moduleClass.asClass.typeSignature
  val ps = (b declaration Ctor).asMethod.paramss.flatten.zipWithIndex
  val args: List[c.Tree] = ps map { case (p, i) =>
    if (srcs contains p.name)
      Select(Ident(src), p.name)
    else bc member TermName(defaulter(Ctor, i)) match { 
      case NoSymbol =>
        if (p.typeSignature.typeSymbol.asClass.isPrimitive) { 
          if (p.typeSignature <:< typeOf[Int]) literal(0).tree
          else if (p.typeSignature <:< typeOf[Double]) literal(0.0).tree
          else ???
        } else literalNull.tree
      case defarg   => Select(mkAttributedRef(bm), defarg.name)
    } 
  } 
  c.Expr(Block(side.toList, Apply(Select(New(mkAttributedIdent(b.typeSymbol)), Ctor), args)))
} 

:

case class C(a: String, b: Int, c: Double = 2.0, d: Double)
case class K(b: Int, e: String, a: String) { def i() = b }
val res = newFrom[K, C](K(8, "oh", "kay"))
+2

All Articles