caso classe para mapear em Scala
-
22-07-2019 - |
Pergunta
Existe uma boa maneira eu posso converter uma instância case class
Scala, por exemplo.
case class MyClass(param1: String, param2: String)
val x = MyClass("hello", "world")
em um mapeamento de algum tipo, por exemplo.
getCCParams(x) returns "param1" -> "hello", "param2" -> "world"
O que funciona para qualquer classe caso, não apenas os pré-definidos. Eu descobri que você pode puxar o nome da classe caso por escrever um método que interroga o subjacente classe de produto, por exemplo.
def getCCName(caseobj: Product) = caseobj.productPrefix
getCCName(x) returns "MyClass"
Então, eu estou procurando uma solução semelhante, mas para os campos de classe caso. Eu imagino uma solução pode ter que usar a reflexão Java, mas eu odiaria para escrever algo que pode quebrar em uma versão futura do Scala se a implementação subjacente de casos classes de alterações.
Atualmente estou trabalhando em um servidor Scala e definindo o protocolo e todas as suas mensagens e exceções usando classes de casos, pois eles são uma bela construção tal, concisa para isso. Mas então, necessidade de traduzi-los em um mapa Java para enviar sobre a camada de mensagens para qualquer aplicação cliente para uso. Minha implementação atual, apenas define a tradução para cada classe caso separadamente, mas seria bom para encontrar uma solução generalizada.
Solução
Isso deve funcionar:
def getCCParams(cc: AnyRef) =
(Map[String, Any]() /: cc.getClass.getDeclaredFields) {(a, f) =>
f.setAccessible(true)
a + (f.getName -> f.get(cc))
}
Outras dicas
Como as classes de casos estender um produtos pode simplesmente usar .productIterator
para obter campo valores:
def getCCParams(cc: Product) = cc.getClass.getDeclaredFields.map( _.getName ) // all field names
.zip( cc.productIterator.to ).toMap // zipped with all values
Ou, alternativamente:
def getCCParams(cc: Product) = {
val values = cc.productIterator
cc.getClass.getDeclaredFields.map( _.getName -> values.next ).toMap
}
Uma das vantagens do produto é que você não precisa chamar setAccessible
no campo de ler seu valor. Outra é que productIterator não usa reflexão.
Note que este exemplo funciona com aulas de caso simples que não se estendem outras classes e não declaram campos fora do construtor.
Se olhares qualquer um para uma versão recursiva, aqui é a modificação de @ solução de Andrejs:
def getCCParams(cc: Product): Map[String, Any] = {
val values = cc.productIterator
cc.getClass.getDeclaredFields.map {
_.getName -> (values.next() match {
case p: Product if p.productArity > 0 => getCCParams(p)
case x => x
})
}.toMap
}
Ele também expande o caso-classes aninhadas em mapas em qualquer nível de aninhamento.
Aqui está uma variação simples se você não se preocupam com tornando-se uma função genérica:
case class Person(name:String, age:Int)
def personToMap(person: Person): Map[String, Any] = {
val fieldNames = person.getClass.getDeclaredFields.map(_.getName)
val vals = Person.unapply(person).get.productIterator.toSeq
fieldNames.zip(vals).toMap
}
scala> println(personToMap(Person("Tom", 50)))
res02: scala.collection.immutable.Map[String,Any] = Map(name -> Tom, age -> 50)
Você pode usar disforme.
Let
case class X(a: Boolean, b: String,c:Int)
case class Y(a: String, b: String)
Definir uma representação LabelledGeneric
import shapeless._
import shapeless.ops.product._
import shapeless.syntax.std.product._
object X {
implicit val lgenX = LabelledGeneric[X]
}
object Y {
implicit val lgenY = LabelledGeneric[Y]
}
Definir duas typeclasses para fornecer os métodos toMap
object ToMapImplicits {
implicit class ToMapOps[A <: Product](val a: A)
extends AnyVal {
def mkMapAny(implicit toMap: ToMap.Aux[A, Symbol, Any]): Map[String, Any] =
a.toMap[Symbol, Any]
.map { case (k: Symbol, v) => k.name -> v }
}
implicit class ToMapOps2[A <: Product](val a: A)
extends AnyVal {
def mkMapString(implicit toMap: ToMap.Aux[A, Symbol, Any]): Map[String, String] =
a.toMap[Symbol, Any]
.map { case (k: Symbol, v) => k.name -> v.toString }
}
}
Em seguida, você pode usá-lo como este.
object Run extends App {
import ToMapImplicits._
val x: X = X(true, "bike",26)
val y: Y = Y("first", "second")
val anyMapX: Map[String, Any] = x.mkMapAny
val anyMapY: Map[String, Any] = y.mkMapAny
println("anyMapX = " + anyMapX)
println("anyMapY = " + anyMapY)
val stringMapX: Map[String, String] = x.mkMapString
val stringMapY: Map[String, String] = y.mkMapString
println("anyMapX = " + anyMapX)
println("anyMapY = " + anyMapY)
}
que imprime
anyMapX = Mapa (c -> 26, b -> bicicleta, a -> true)
anyMapY = Mapa (b -> segundo, a -> primeiro)
stringMapX = Mapa (c -> 26, b -> bicicleta, a -> true)
stringMapY = Mapa (b -> segundo, a -> primeiro)
Para as classes de casos aninhados, (mapas, assim, aninhados) verificar outra resposta
Solução com ProductCompletion
de pacote intérprete:
import tools.nsc.interpreter.ProductCompletion
def getCCParams(cc: Product) = {
val pc = new ProductCompletion(cc)
pc.caseNames.zip(pc.caseFields).toMap
}
Se acontecer de você estar usando Json4s, você pode fazer o seguinte:
import org.json4s.{Extraction, _}
case class MyClass(param1: String, param2: String)
val x = MyClass("hello", "world")
Extraction.decompose(x)(DefaultFormats).values.asInstanceOf[Map[String,String]]
Iniciando Scala 2.13
, case class
es (como implementações de Product
) são fornecidos com uma productElementNames método que retorna um iterador sobre os nomes de seus campos.
Por fechando nomes de campo com valores de campo obtidos com productIterator podemos genericamente obter o Map
associado:
// case class MyClass(param1: String, param2: String)
// val x = MyClass("hello", "world")
(x.productElementNames zip x.productIterator).toMap
// Map[String,Any] = Map("param1" -> "hello", "param2" -> "world")
Eu não sei sobre bom ... mas isso parece funcionar, pelo menos para este exemplo muito básico. Ele provavelmente precisa de algum trabalho, mas pode ser suficiente para você começar? Basicamente, ele filtra todos os métodos "conhecidos" de uma classe caso (ou qualquer outra classe: /)
object CaseMappingTest {
case class MyCase(a: String, b: Int)
def caseClassToMap(obj: AnyRef) = {
val c = obj.getClass
val predefined = List("$tag", "productArity", "productPrefix", "hashCode",
"toString")
val casemethods = c.getMethods.toList.filter{
n =>
(n.getParameterTypes.size == 0) &&
(n.getDeclaringClass == c) &&
(! predefined.exists(_ == n.getName))
}
val values = casemethods.map(_.invoke(obj, null))
casemethods.map(_.getName).zip(values).foldLeft(Map[String, Any]())(_+_)
}
def main(args: Array[String]) {
println(caseClassToMap(MyCase("foo", 1)))
// prints: Map(a -> foo, b -> 1)
}
}
commons.mapper.Mappers.Mappers.beanToMap(caseClassBean)
Detalhes: https://github.com/hank-whu/common4s