Skip to content

Instantly share code, notes, and snippets.

@hamnis
Last active September 23, 2019 20:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hamnis/9ade4ee63acb92fb3c88cd25713e3ed9 to your computer and use it in GitHub Desktop.
Save hamnis/9ade4ee63acb92fb3c88cd25713e3ed9 to your computer and use it in GitHub Desktop.
package xmldecoder
import shapeless._, labelled.{field, FieldType}
object automatic {
implicit def DecoderNewType[A <: AnyVal, B](
implicit gen: Lazy[Generic.Aux[A, B :: HNil]],
decoder: Decoder[B]
): Decoder[A] =
decoder.map[A](b => gen.value.from(b :: HNil))
implicit val decodeHNil: Decoder[HNil] = Decoder.const(HNil)
implicit def decodeHCons[K <: Symbol, H, T <: HList](
implicit key: Witness.Aux[K],
decodeH: Decoder[H],
decodeT: Decoder[T]
): Decoder[FieldType[K, H] :: T] =
xml =>
for {
h <- Decoder.childAs[H](key.value.name).decode(xml)
t <- decodeT.decode(xml)
} yield field[K](h) :: t
implicit def decodeGeneric[A, R](implicit gen: LabelledGeneric.Aux[A, R], decodeR: Decoder[R]): Decoder[A] = {
decodeR.map(gen.from)
}
}
package xmldecoder
import scala.util.Try
import scala.xml.{Node, NodeSeq}
case class XMLDecodeException(path: String, message: String) extends Exception {
override def getMessage: String = s"[$path] - $message"
}
trait Decoder[A] {
def decode(node: NodeSeq): Either[XMLDecodeException, A]
def map[B](f: A => B): Decoder[B] = decode(_).map(f)
def flatMap[B](f: A => Decoder[B]): Decoder[B] =
node =>
for {
a <- decode(node)
b <- f(a).decode(node)
} yield b
def tryMap[B](f: A => B): Decoder[B] =
elem =>
decode(elem).flatMap(
v =>
Try(f(v)).toEither.left.map { t =>
val label = elem match {
case node: Node => node.label
case _ => ""
}
XMLDecodeException(label, t.getMessage)
}
)
}
object Decoder {
def const[A](value: A): Decoder[A] = _ => Right(value)
def apply[A](implicit decoder: Decoder[A]): Decoder[A] = decoder
implicit val decodeString: Decoder[String] = elem =>
if (elem.isEmpty) Left(XMLDecodeException("StringDecoder", "Empty elements"))
else Right(elem.text.trim)
implicit val decodeInt: Decoder[Int] = decodeString.tryMap(_.toInt)
implicit val decodeLong: Decoder[Long] = decodeString.tryMap(_.toLong)
implicit val decodeDouble: Decoder[Double] = decodeString.tryMap(_.toDouble)
implicit val decodeUnit: Decoder[Unit] = elem =>
if (elem.isEmpty) Right(()) else Left(XMLDecodeException("UnitDecoder", "Non-empty elements"))
implicit def option[A: Decoder]: Decoder[Option[A]] =
elem =>
if (elem.isEmpty || elem.text.isBlank) Right(None)
else Decoder[A].decode(elem).map(Some(_))
implicit def list[A: Decoder]: Decoder[List[A]] =
nodeSeq => {
val from = Decoder[A]
Right(nodeSeq.flatMap(node => from.decode(node).toSeq).toList)
}
def childAs[A: Decoder](name: String): Decoder[A] =
elem =>
Decoder[A].decode(elem \ name) match {
case Left(XMLDecodeException(p, m)) =>
Left(XMLDecodeException(if (p.isBlank) name else s"$name/$p", m))
case Right(value) => Right(value)
}
}
trait XMLSyntax {
implicit class ExtractSyntax(nodeSeq: NodeSeq) {
def as[A: Decoder]: Either[XMLDecodeException, A] =
Decoder[A].decode(nodeSeq)
def childAs[A: Decoder](name: String): Either[XMLDecodeException, A] =
Decoder.childAs[A](name).decode(nodeSeq)
}
}
object syntax extends XMLSyntax
package xmldecoder
import org.scalatest.FunSuite
class GenericDecoderTest extends FunSuite {
case class Foo(name: String)
case class Bar(barry: String, foo: Foo)
import automatic._
test("simple decoder") {
val xml = <foo>
<name>Name</name>
</foo>
assert(Decoder[Foo].decode(xml) === Right(Foo("Name")))
}
test("Nested decoder") {
val xml = <baz>
<barry>Barry</barry>
<foo>
<name>Name</name>
</foo>
</baz>
assert(Decoder[Bar].decode(xml) === Right(Bar("Barry", Foo("Name"))))
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment