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.

Foi útil?

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 classes (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

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top