Skip to content

Instantly share code, notes, and snippets.

@limansky
Last active June 25, 2017 09:39
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save limansky/7c91a3deaa25fc0ec7339ea191d4a54a to your computer and use it in GitHub Desktop.
Save limansky/7c91a3deaa25fc0ec7339ea191d4a54a to your computer and use it in GitHub Desktop.
Read Map[String, String] to case class
package me.limansky
import cats.Monoid
import shapeless.labelled.{FieldType, field}
import shapeless.{::, HList, HNil, LabelledGeneric, Lazy, Witness}
trait MapReader[T] {
def read(m: Map[String, String]): T
}
object MapReader {
trait ValueDecoder[T] {
def decode(s: String): T
}
implicit val stringDecoder: ValueDecoder[String] = new ValueDecoder[String] {
override def decode(s: String): String = s
}
implicit val intDecoder: ValueDecoder[Int] = new ValueDecoder[Int] {
override def decode(s: String): Int = s.toInt
}
def apply[T](implicit ev: MapReader[T]): MapReader[T] = ev
implicit val hnilReader: MapReader[HNil] = new MapReader[HNil] {
override def read(m: Map[String, String]) = HNil
}
implicit def hconsValue[K <: Symbol, H, T <: HList](implicit
witness: Witness.Aux[K],
hDecoder: ValueDecoder[H],
hMonoid: Monoid[H],
tReader: MapReader[T]
): MapReader[FieldType[K, H] :: T] = new MapReader[FieldType[K, H] :: T] {
override def read(m: Map[String, String]) = {
val name = witness.value.name
field[K](m.get(name).map(hDecoder.decode).getOrElse(hMonoid.empty)) :: tReader.read(m)
}
}
implicit def hconsProduct[K <: Symbol, H, T <: HList](implicit
witness: Witness.Aux[K],
hReader: Lazy[MapReader[H]],
tReader: MapReader[T]
): MapReader[FieldType[K, H] :: T] = new MapReader[FieldType[K, H] :: T] {
override def read(m: Map[String, String]) = {
field[K](hReader.value.read(m)) :: tReader.read(m)
}
}
implicit def genericReader[A, R](implicit
gen: LabelledGeneric.Aux[A, R],
mapReader: Lazy[MapReader[R]]
): MapReader[A] = new MapReader[A] {
override def read(m: Map[String, String]) = gen.from(mapReader.value.read(m))
}
}
package me.limansky
import org.scalatest.{FlatSpec, Matchers}
class MapReaderTest extends FlatSpec with Matchers {
import cats.instances.all._
"MapReader" should "read simple case classes" in {
case class Test(a: String, b: Int)
MapReader[Test].read(Map("a" -> "foo", "b" -> "7")) shouldEqual Test("foo", 7)
}
it should "support nested classes" in {
case class Inner(one: Int, two: Int)
case class Outer(three: String, inner: Inner)
MapReader[Outer].read(Map("one" -> "1", "two" -> "2", "three" -> "3")) shouldEqual Outer("3", Inner(1, 2))
}
it should "init missing data" in {
case class Inner(one: Int, two: Int)
case class Outer(three: String, inner: Inner)
MapReader[Outer].read(Map("one" -> "42")) shouldEqual Outer("", Inner(42, 0))
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment