2011-05-31 12 views
16

Si tengo un Map[String,String]("url" -> "xxx", "title" -> "yyy"), ¿hay alguna manera de transformarlo genéricamente en case class Image(url:String, title:String)?¿Cómo puedo transformar un mapa en una clase de caso en Scala?

puedo escribir un ayudante:

object Image{ 
    def fromMap(params:Map[String,String]) = Image(url=params("url"), title=params("title")) 
} 

pero ¿hay una manera de escribir genéricamente esta vez para un mapa a cualquier clase de caso?

Respuesta

8

En primer lugar, hay algunas alternativas seguras que puede hacer si solo quiere acortar su código. El objeto acompañante puede ser tratada como una función de lo que podría utilizar algo como esto:

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

Esto devolverá una opción que contiene el resultado. Otra posibilidad es utilizar los ApplicativeBuilder s en Scalaz cuales internamente hacer casi lo mismo pero con una sintaxis más agradable:

import scalaz._, Scalaz._ 
(m.get("url") |@| m.get("title"))(Image) 

Si realmente necesita hacer esto a través de la reflexión a continuación, la manera más fácil sería utilizar Paranamer (ya que el ascensor -Framework lo hace). Paranamer puede restaurar los nombres de los parámetros inspeccionando el bytecode, por lo que hay un golpe de rendimiento y no funcionará en todos los entornos debido a problemas con el cargador de clases (el REPL, por ejemplo).Si se restringe a sí mismo a clases con sólo String parámetros del constructor, entonces podría hacerlo de esta manera:

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) 

(Tenga en cuenta que este ejemplo puede escoger un constructor por defecto, ya que no comprueba el recuento de parámetro que usted quiere lo hacen)

+0

Los parámetros de tipo de build2 son directamente proporcionales al número de campos. No tan limpio, creo. –

-2

Esto no se puede hacer, ya que necesitaría obtener los nombres de los parámetros del método de aplicación del objeto complementario y simplemente no están disponibles a través de la reflexión. Si tiene muchas de estas clases de casos, podría analizar sus declaraciones y generar los métodos fromMap.

+2

No están disponibles a través de la reflexión estándar de Java, pero puede probar suerte analizar el [ 'ScalaSignature'] (http://www.scala-lang.org/sid/10) bytes ... –

2
No

una respuesta completa a su pregunta, pero un comienzo ...

Se puede hacer, pero probablemente se volverá más complicado de lo que pensaba. Cada clase Scala generada se anota con la anotación Java ScalaSignature, cuyo miembro bytes se puede analizar para proporcionarle los metadatos que necesitaría (incluidos los nombres de los argumentos). Sin embargo, el formato de esta firma no es API, por lo que tendrá que analizarlo usted mismo (y es probable que cambie la forma en que lo analiza con cada nueva versión principal de Scala).

Quizás el mejor lugar para comenzar es la biblioteca lift-json, que tiene la capacidad de crear instancias de clases de casos basadas en datos JSON.

Actualización: Creo ascensor-JSON utiliza realmente Paranamer para hacer esto, y por lo tanto no puede analizar los bytes de ScalaSignature ... cual hace que esta técnica funcione para las clases no Scala, también.

Actualización 2: En cambio, consulte Moritz's answer, quien está mejor informado que yo.

+0

¿por qué no simplemente crear datos json y dejar que lift-json se encargue del resto? de esa forma no tendría que actualizarlo él mismo con cada nueva versión de scala y no tendría que analizar los bytes de ScalaSignature. El rendimiento sería menos que óptimo, por supuesto, pero eso podría no ser un problema para el PO. ¿Me estoy perdiendo de algo? –

+0

@Kim Oye, ese es un buen camino a seguir, si el OP está dispuesto a crear JSON solo para ese fin. Alternativamente, tal vez sea posible reutilizar lift-json más directamente con un 'Map [String, String]' como entrada ... –

+0

gracias por las sugerencias. Pasar de Map a JSON string a lift-json parsing a case class parece una gran cantidad de procesamiento de serialización/deserialización innecesario. –

6

Aquí es una solución utilizando incorporado Scala/java reflexión:

def createCaseClass[T](vals : Map[String, Object])(implicit cmf : ClassManifest[T]) = { 
     val ctor = cmf.erasure.getConstructors().head 
     val args = cmf.erasure.getDeclaredFields().map(f => vals(f.getName)) 
     ctor.newInstance(args : _*).asInstanceOf[T] 
    } 

utilizarlo:

val image = createCaseClass[Image](Map("url" -> "xxx", "title" -> "yyy")) 
+0

Este es un enfoque interesante; sin embargo, ¿cómo evitar las excepciones de 'tipo de argumento desajuste 'al crear una instancia de la clase? – bachr

+0

Esta es la respuesta más concisa a esta pregunta que he visto. Sin embargo, usa API que ahora está en desuso. Esto se actualiza fácilmente mediante el uso de "ClassTag" en lugar de "ClassManifest" y "runtimeClass" en lugar de "borrado" –

0

Hay un truco que puede transformar el mapa en json y luego en la clase de casos. Solía ​​aspersión JSON

import spray.json._ 

object MainClass2 extends App { 
    val mapData: Map[Any, Any] = 
    Map(
     "one" -> "1", 
     "two" -> 2, 
     "three" -> 12323232123887L, 
     "four" -> 4.4, 
     "five" -> false 
    ) 

    implicit object AnyJsonFormat extends JsonFormat[Any] { 
    def write(x: Any): JsValue = x match { 
     case int: Int   => JsNumber(int) 
     case long: Long   => JsNumber(long) 
     case double: Double  => JsNumber(double) 
     case string: String  => JsString(string) 
     case boolean: Boolean if boolean => JsTrue 
     case boolean: Boolean if !boolean => JsFalse 
    } 
    def read(value: JsValue): Any = value match { 
     case JsNumber(int) => int.intValue() 
     case JsNumber(long) => long.longValue() 
     case JsNumber(double) => double.doubleValue() 
     case JsString(string) => string 
     case JsTrue  => true 
     case JsFalse  => false 
    } 
    } 

    import ObjJsonProtocol._ 
    val json = mapData.toJson 
    val result: TestObj = json.convertTo[TestObj] 
    println(result) 

} 

final case class TestObj(one: String, two: Int, three: Long, four: Double, five: Boolean) 

object ObjJsonProtocol extends DefaultJsonProtocol { 
    implicit val objFormat: RootJsonFormat[TestObj] = jsonFormat5(TestObj) 
} 

y el uso de esta dependencia en la acumulación SBT:

"io.spray"   %% "spray-json"  % "1.3.3" 
Cuestiones relacionadas