Skip to content

Instantly share code, notes, and snippets.

@SystemFw
Last active April 23, 2021 07:53
Show Gist options
  • Save SystemFw/03d66d65e471c98f02ba27d7180465b1 to your computer and use it in GitHub Desktop.
Save SystemFw/03d66d65e471c98f02ba27d7180465b1 to your computer and use it in GitHub Desktop.
Typed schema conversion with shapeless
object Conversions {
import cats._, implicits._, data.ValidatedNel
import mouse._, string._, option._
import shapeless._, labelled._
private type Result[A] = ValidatedNel[ParseFailure, A]
case class ParseFailure(error: String)
trait Convert[V] {
def parse(input: String): Result[V]
}
object Convert {
def to[V](input: String)(implicit C: Convert[V]): Result[V] =
C.parse(input)
def instance[V](body: String => Result[V]): Convert[V] = new Convert[V] {
def parse(input: String): Result[V] = body(input)
}
implicit def booleans: Convert[Boolean] =
Convert.instance(
s =>
s.parseBooleanValidated
.leftMap(e => ParseFailure(s"Not a Boolean ${e.getMessage}"))
.toValidatedNel)
implicit def ints: Convert[Int] =
Convert.instance(
s =>
s.parseIntValidated
.leftMap(e => ParseFailure(s"Not an Int ${e.getMessage}"))
.toValidatedNel)
implicit def strings: Convert[String] = Convert.instance(s => s.validNel)
}
sealed trait Schema[A] {
def readFrom(input: Map[String, String]): ValidatedNel[ParseFailure, A]
}
object Schema {
def of[A](implicit s: Schema[A]): Schema[A] = s
private def instance[A](
body: Map[String, String] => Result[A]): Schema[A] = new Schema[A] {
def readFrom(input: Map[String, String]): Result[A] = body(input)
}
implicit val noOp: Schema[HNil] =
Schema.instance(_ => HNil.validNel)
implicit def parsing[K <: Symbol, V: Convert, T <: HList](
implicit key: Witness.Aux[K],
next: Schema[T]): Schema[FieldType[K, V] :: T] =
Schema.instance { input =>
val fieldName = key.value.name
val parsedField = input
.get(fieldName)
.cata(entry => Convert.to[V](entry),
ParseFailure(s"$fieldName is missing").invalidNel)
.map(f => field[K](f))
(parsedField, next.readFrom(input)).mapN(_ :: _)
}
implicit def classes[A, R <: HList](
implicit repr: LabelledGeneric.Aux[A, R],
schema: Schema[R]): Schema[A] =
Schema.instance { input =>
schema.readFrom(input).map(x => repr.from(x))
}
}
}
import Conversions._
case class Foo(a: String, b: Int, c: Boolean)
def m: Map[String, String] = Map("a" -> "hello", "c" -> "true", "b" -> "100")
def e: Map[String, String] = Map("c" -> "true", "b" -> "a100")
val schema = Schema.of[Foo]
val result = schema.readFrom(m)
// res0: ValidatedNel[ParseFailure, Foo] = Valid(Foo(hello,100,true))
val error = schema.readFrom(e)
// res1: ValidatedNel[ParseFailure, Foo] =
// Invalid(NonEmptyList(ParseFailure(a is missing), ParseFailure(Not an Int For input string: "a100")))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment