Shapeless code to convert Map[String, Any] to case

2020-04-21 08:02发布

问题:

I'm trying to use this https://stackoverflow.com/a/31641779/1586965 (How to use shapeless to convert generic Map[String, Any] to case class inside generic function?) to process

case class Address(street: String, zip: Int)
case class PersonOptionalAddress(name: String, address: Option[Address])

I have a test that fails:

"Convert Map to PersonOptionalAddress Some" in {
  CaseClassFromMap[PersonOptionalAddress](Map(
    "name" -> "Tom",
    "address" -> Some(Map("street" -> "Jefferson st", "zip" -> 10000))
  )) must_=== PersonOptionalAddress("Tom", Some(Address("Jefferson st", 10000)))
}

with

java.util.NoSuchElementException: None.get

If the substructure is not nested, or it is None, then the tests work fine.

I've also tried this, but it doesn't work either

"Convert Map to PersonOptionalAddress Some" in {
  CaseClassFromMap[PersonOptionalAddress](Map(
    "name" -> "Tom",
    "address" -> Map("x" -> Map("street" -> "Jefferson st", "zip" -> 10000))
  )) must_=== PersonOptionalAddress("Tom", Some(Address("Jefferson st", 10000)))
}

回答1:

If you want the code to work with PersonOptionalAddress you should add one more instance of the type class so that it will work also with Map( "name" -> "Tom", "address" -> Some(Map ...) )

implicit def hconsFromMap0opt[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, Option[V]] :: T] =
      new FromMap[FieldType[K, Option[V]] :: T] {
        def apply(m: Map[String, Any]): Option[FieldType[K, Option[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](Some(gen.from(h))) :: t).orElse(for {
          v <- m.get(witness.value.name)
          r1 <- Typeable[Option[Map[String, Any]]].cast(v)
          opt = for {
            r <- r1
            h <- fromMapH(r)
          } yield gen.from(h)
          t <- fromMapT(m)
        } yield field[K](opt) :: t)
      }

The whole code

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

object App {

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

  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
      }

    implicit def hconsFromMap0opt[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, Option[V]] :: T] =
      new FromMap[FieldType[K, Option[V]] :: T] {
        def apply(m: Map[String, Any]): Option[FieldType[K, Option[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](Some(gen.from(h))) :: t).orElse(for {
          v <- m.get(witness.value.name)
          r1 <- Typeable[Option[Map[String, Any]]].cast(v)
          opt = for {
            r <- r1
            h <- fromMapH(r)
          } yield gen.from(h)
          t <- fromMapT(m)
        } yield field[K](opt) :: t)
      }

  trait CaseClassFromMap[P <: Product] {
    def apply(m: Map[String, Any]): Option[P]
  }

  object CaseClassFromMap {
    implicit def mk[P <: Product, R <: HList](implicit gen: LabelledGeneric.Aux[P, R],
                                              fromMap: FromMap[R]): CaseClassFromMap[P] = new CaseClassFromMap[P] {
      def apply(m: Map[String, Any]): Option[P] = fromMap(m).map(gen.from)
    }

    def apply[P <: Product](map: Map[String, Any])(implicit fromMap: CaseClassFromMap[P]): P = fromMap(map).get
  }

  case class Address(street: String, zip: Int)
  case class PersonOptionalAddress(name: String, address: Option[Address])
  case class PersonAddress(name: String, address: Address)

  def main(args: Array[String]): Unit = {
    println(
      CaseClassFromMap[PersonAddress](Map(
        "name" -> "Tom",
        "address" -> Map("street" -> "Jefferson st", "zip" -> 10000)
      ))
    )//PersonAddress(Tom,Address(Jefferson st,10000))

    println(
      CaseClassFromMap[PersonOptionalAddress](Map(
        "name" -> "Tom",
        "address" -> Map("street" -> "Jefferson st", "zip" -> 10000)
      ))
    )//PersonOptionalAddress(Tom,Some(Address(Jefferson st,10000)))

    println(
      CaseClassFromMap[PersonOptionalAddress](Map(
        "name" -> "Tom",
        "address" -> Some(Map("street" -> "Jefferson st", "zip" -> 10000))
      ))
    )//PersonOptionalAddress(Tom,Some(Address(Jefferson st,10000)))

    println(
      CaseClassFromMap[PersonOptionalAddress](Map(
        "name" -> "Tom",
        "address" -> None
      ))
    )//PersonOptionalAddress(Tom,None)

  }
}