Skip to content

Instantly share code, notes, and snippets.

@knutwalker
Last active April 12, 2020 15:21
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save knutwalker/ac18c4c59e3fd96a771e to your computer and use it in GitHub Desktop.
Save knutwalker/ac18c4c59e3fd96a771e to your computer and use it in GitHub Desktop.
Converting Map[String,Any] to a case class using Shapeless - http://stackoverflow.com/q/31640565/2996265
/*
* Based on http://stackoverflow.com/a/31641779/2996265
*
* Changed:
* - use https://github.com/knutwalker/validation to accumulate errors
* - support lists as well (polymorphic on higher kinds would be better, though)
* - use -Xexperimental for java8 style SAM support
*/
import validation.{ NonEmptyVector, Result }
import shapeless._
import shapeless.labelled._
trait FromMap[R <: HList] extends ((Map[String, Any], Option[String]) ⇒ Result[String, R]) {
def apply(m: Map[String, Any], parent: Option[String]): Result[String, R]
}
trait LowPriorityFromMap0 {
implicit def hconsFromMap0[K <: Symbol, V, T <: HList](
implicit
K: Witness.Aux[K],
V: Typeable[V],
T: Lazy[FromMap[T]]
): FromMap[FieldType[K, V] :: T] = (m, p) ⇒ {
val name = K.value.name
val fullName = p.fold(name)(_ + s".$name")
val value = Result
.fromOption(m.get(name), s"There is no entry named [$fullName] in the map.")
.flatMap(v ⇒ Result.fromOption(V.cast(v), s"The value [$v] under [$fullName] is not of type [${V.describe}]"))
val tail = T.value(m, p)
(value and tail) ((v, t) ⇒ field[K](v) :: t)
}
}
trait LowPriorityFromMap1 extends LowPriorityFromMap0 {
implicit def hconsFromMap1[K <: Symbol, V, R <: HList, T <: HList](
implicit
K: Witness.Aux[K],
V: LabelledGeneric.Aux[V, R],
R: Lazy[FromMap[R]],
T: Lazy[FromMap[T]]
): FromMap[FieldType[K, V] :: T] = (m, p) ⇒ {
val name = K.value.name
val fullName = p.fold(name)(_ + s".$name")
val value = Result
.fromOption(m.get(name), s"There is no entry named [$fullName] in the map.")
.flatMap(k ⇒ Result.fromOption(Typeable[Map[String, Any]].cast(k), s"The value under [$fullName] is not a nested map."))
.flatMap(v ⇒ R.value(v, Some(fullName)))
val tail = T.value(m, p)
(value and tail) ((r, t) ⇒ field[K](V.from(r)) :: t)
}
}
object FromMap extends LowPriorityFromMap1 {
private[this] val some_hnil = Result.valid(HNil)
implicit val hnilFromMap: FromMap[HNil] = (m, p) ⇒ some_hnil
implicit def hconsFromMap2[K <: Symbol, V, R <: HList, T <: HList](
implicit
K: Witness.Aux[K],
V: LabelledGeneric.Aux[V, R],
R: Lazy[FromMap[R]],
T: Lazy[FromMap[T]]
): FromMap[FieldType[K, List[V]] :: T] = (m, p) ⇒ {
val name = K.value.name
val fullName = p.fold(name)(_ + s".$name")
val value: Result[String, List[R]] = Result
.fromOption(m.get(name), s"There is no entry named [$fullName] in the map.")
.flatMap(k ⇒ Result.fromOption(Typeable[List[Map[String, Any]]].cast(k), s"The value under [$fullName] is not a nested list of maps."))
.flatMap(xs ⇒ Result.traverse(xs.zipWithIndex)({ case (v, i) ⇒ R.value(v, Some(s"$fullName.$i"))}))
val tail = T.value(m, p)
(value and tail) ((r, t) ⇒ field[K](r.map(V.from)) :: t)
}
class ConverterHelper[A] {
def run[R <: HList](m: Map[String, Any])(
implicit
A: LabelledGeneric.Aux[A, R],
R: FromMap[R]
): Result[String, A] =
R(m, None).map(A.from)
}
def to[A](implicit A: LabelledGeneric[A]) = new ConverterHelper[A]
}
object Test extends App {
implicit final class ResultOps[A](private val result: Result[String, A]) extends AnyVal {
def getAll: NonEmptyVector[String] =
result.fold(identity, x ⇒ NonEmptyVector(x.toString))
}
case class Address(street: String, zip: Int)
case class Person(name: String, addresses: List[Address])
def run(maps: Map[String, Any]*): Unit = maps foreach { m ⇒
val p = FromMap.to[Person].run(m)
p.getAll.toVector.foreach(println)
println("==========")
}
val mp0: Map[String, Any] = Map(
"name" -> "Tom",
"addresses" -> List(Map("street" -> "Jefferson st", "zip" -> 10000): Map[String, Any])
)
val mp01: Map[String, Any] = Map(
"name" -> "Tom",
"addresses" -> Vector(Map("street" -> "Jefferson st", "zip" -> 10000): Map[String, Any])
)
val mp1: Map[String, Any] = Map(
"names" -> "Tom",
"addresses" -> List(Map("street" -> "Jefferson st", "zip" -> 10000): Map[String, Any])
)
val mp2: Map[String, Any] = Map(
"name" -> "Tom",
"address" -> Map("street" -> "Jefferson st", "zip" -> 10000)
)
val mp3: Map[String, Any] = Map(
"name" -> "Tom",
"addresses" -> Map("streets" -> "Jefferson st", "zip" -> "10000")
)
val mp4: Map[String, Any] = Map(
"name" -> "Tom",
"addresses" -> List(Map("streets" -> "Jefferson st", "zip" -> "10000"))
)
val mp5: Map[String, Any] = Map(
"name" -> "Tom",
"addresses" -> List(Map("street" -> "Jefferson st", "zip" -> 10000): Map[String, Any], "foo")
)
val mp6: Map[String, Any] = Map(
"name" -> 't',
"addresses" -> List(
Map("streets" -> "Jefferson st", "zip" -> "10000"): Map[String, Any],
Map("street" -> 'b', "sip" -> 10000): Map[String, Any])
)
run(mp0, mp01, mp1, mp2, mp3, mp4, mp5, mp6)
}
Person(Tom,List(Address(Jefferson st,10000)))
==========
The value under [addresses] is not a nested list of maps.
==========
There is no entry named [name] in the map.
==========
There is no entry named [addresses] in the map.
==========
The value under [addresses] is not a nested list of maps.
==========
There is no entry named [addresses.0.street] in the map.
The value [10000] under [addresses.0.zip] is not of type [Int]
==========
The value under [addresses] is not a nested list of maps.
==========
The value [t] under [name] is not of type [String]
There is no entry named [addresses.0.street] in the map.
The value [10000] under [addresses.0.zip] is not of type [Int]
The value [b] under [addresses.1.street] is not of type [String]
There is no entry named [addresses.1.zip] in the map.
==========
@elyphas
Copy link

elyphas commented Apr 12, 2020

Hi, I am trying to test the code on my machine, but I have an error.
Could You tell me, please, Why I have this error?

not found: object validation
[error] import validation.{ NonEmptyVector, Result }

@elyphas
Copy link

elyphas commented Apr 12, 2020

sorry, I think here is the reason:
validation

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment