If I have a Map[String,String]("url" -> "xxx", "title" -> "yyy")
, is there an way to generically transform it into a case class Image(url:String, title:String)
?
I can write a helper:
object Image{
def fromMap(params:Map[String,String]) = Image(url=params("url"), title=params("title"))
}
but is there a way to generically write this once for a map to any case class?
First off, there are some safe alternatives you could do if you just want to shorten your code. The companion object can be treated as a function so you could use something like this:
def build2[A,B,C](m: Map[A,B], f: (B,B) => C)(k1: A, k2: A): Option[C] = for {
v1 <- m.get(k1)
v2 <- m.get(k2)
} yield f(v1, v2)
build2(m, Image)("url", "title")
This will return an option containing the result. Alternatively you could use the ApplicativeBuilder
s in Scalaz which internally do almost the same but with a nicer syntax:
import scalaz._, Scalaz._
(m.get("url") |@| m.get("title"))(Image)
If you really need to do this via reflection then the easiest way would be to use Paranamer (as the Lift-Framework does). Paranamer can restore the parameter names by inspecting the bytecode so there is a performance hit and it will not work in all environments due to classloader issues (the REPL for example). If you restrict yourself to classes with only String
constructor parameters then you could do it like this:
val pn = new CachingParanamer(new BytecodeReadingParanamer)
def fill[T](m: Map[String,String])(implicit mf: ClassManifest[T]) = for {
ctor <- mf.erasure.getDeclaredConstructors.filter(m => m.getParameterTypes.forall(classOf[String]==)).headOption
parameters = pn.lookupParameterNames(ctor)
} yield ctor.newInstance(parameters.map(m): _*).asInstanceOf[T]
val img = fill[Image](m)
(Note that this example can pick a default constructor as it does not check for the parameter count which you would want to do)