Converting Map[String,Any] to a case class using Shapeless

我的梦境 提交于 2019-12-17 10:32:44

问题


The question here asks about mapping a case class to a Map[String,Any]. I was wondering what would be the other way around, converting Map[String,Any] to a case class. Given the following map:

val mp = Map("name" -> "Tom", "address" -> Map("street" -> "Jefferson st", "zip" -> 10000))

Convert it to a case class of Person:

case class Person(name:String, address:Address)
case class Address(street:String, zip:Int)

val p = Person("Tom", Address("Jefferson st", 10000))

with something like this:

val newP = mp.asCC[Person]
assert(newP.get == p)

How should I do that with Shapeless.


回答1:


Here's an off-the-cuff, mostly untested solution. First for the type class:

import shapeless._, labelled.{ FieldType, field }

trait FromMap[L <: HList] {
  def apply(m: Map[String, Any]): Option[L]
}

And then the instances:

trait LowPriorityFromMap {
  implicit def hconsFromMap1[K <: Symbol, V, T <: HList](implicit
    witness: Witness.Aux[K],
    typeable: Typeable[V],
    fromMapT: Lazy[FromMap[T]]
  ): FromMap[FieldType[K, V] :: T] = new FromMap[FieldType[K, V] :: T] {
    def apply(m: Map[String, Any]): Option[FieldType[K, V] :: T] = for {
      v <- m.get(witness.value.name)
      h <- typeable.cast(v)
      t <- fromMapT.value(m)
    } yield field[K](h) :: t
  }
}

object FromMap extends LowPriorityFromMap {
  implicit val hnilFromMap: FromMap[HNil] = new FromMap[HNil] {
    def apply(m: Map[String, Any]): Option[HNil] = Some(HNil)
  }

  implicit def hconsFromMap0[K <: Symbol, V, R <: HList, T <: HList](implicit
    witness: Witness.Aux[K],
    gen: LabelledGeneric.Aux[V, R],
    fromMapH: FromMap[R],
    fromMapT: FromMap[T]
  ): FromMap[FieldType[K, V] :: T] = new FromMap[FieldType[K, V] :: T] {
    def apply(m: Map[String, Any]): Option[FieldType[K, V] :: T] = for {
      v <- m.get(witness.value.name)
      r <- Typeable[Map[String, Any]].cast(v)
      h <- fromMapH(r)
      t <- fromMapT(m)
    } yield field[K](gen.from(h)) :: t
  }
}

And then a helper class for convenience:

class ConvertHelper[A] {
  def from[R <: HList](m: Map[String, Any])(implicit
    gen: LabelledGeneric.Aux[A, R],
    fromMap: FromMap[R]
  ): Option[A] = fromMap(m).map(gen.from(_))
}

def to[A]: ConvertHelper[A] = new ConvertHelper[A]

And the example:

case class Address(street: String, zip: Int)
case class Person(name: String, address: Address)

val mp = Map(
  "name" -> "Tom",
  "address" -> Map("street" -> "Jefferson st", "zip" -> 10000)
)

And finally:

scala> to[Person].from(mp)
res0: Option[Person] = Some(Person(Tom,Address(Jefferson st,10000)))

This will only work for case classes whose members are either Typeable or other case classes whose members are either Typeable or other case classes… (and so on).

Gotcha

Careful not to have scala.reflect.runtime.universe._ in your imports, as this will break the above.



来源:https://stackoverflow.com/questions/31640565/converting-mapstring-any-to-a-case-class-using-shapeless

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!