Last active
April 26, 2017 04:13
-
-
Save negator/b41c875f1166f46eb69e to your computer and use it in GitHub Desktop.
Easy XML typeclasses with Shapeless and Scalaz
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** Domain mapped XML doesnt have to be a boilerplate filled annotational mess. Using shapeless automatic | |
* typeclass derivation and a little scalaz magic, it becomes painless. Requires shapeless-2.1.0 and scala >2.10 | |
* | |
* | |
* import scalaz._ | |
* import scalaz.std._ | |
* import anyVal._ | |
* import list._ | |
* import scala.xml._ | |
* | |
* case class Foo(a: Int) | |
* case class Bar(b: Int, foo: Foo) | |
* case class Goo(g: Int, foo: List[Foo]) | |
* case class Doo(d: Int, bar: List[Bar]) | |
* | |
* val toFoo: ToXml[Foo] = ToXml.deriveInstance | |
* val toBar: ToXml[Bar] = ToXml.deriveInstance | |
* val toGoo: ToXml[Goo] = ToXml.deriveInstance | |
* val toDoo: ToXml[Doo] = ToXml.deriveInstance | |
* val fromDoo: FromXml[Doo] = FromXml.deriveInstance | |
* val from: FromXml[Goo] = FromXml.deriveInstance | |
* | |
* val bar = Bar(2, Foo(2)) | |
* val doo = Doo(1, List(Bar(2, Foo(1)), Bar(5, Foo(2)))) | |
* | |
* | |
* val xml = toDoo.toXml(doo, rootLabel="root") // prints : <root><bar><foo><a>1</a></foo><b>2</b></bar><bar><foo><a>2</a></foo><b>5</b></bar><d>1</d></root> | |
* xml doo = fromDoo.fromXml(xml) // prints : \/-(Doo(1,List(Bar(2,Foo(1)), Bar(5,Foo(2))))) | |
* | |
* | |
* | |
* | |
* | |
* | |
**/ | |
import scalaz.{ Success => _, Failure => _, _ } | |
import Scalaz._ | |
import shapeless.{ `::` => :#:, _ } | |
import scala.xml._ | |
import scala.util._ | |
trait Read[T] { | |
def reads(s: String): Try[T] | |
} | |
trait ReadInstances { | |
implicit val StringReads = new Read[String] { | |
def reads(s: String) = Success(s) | |
} | |
implicit val IntReads = new Read[Int] { | |
def reads(s: String) = Try(s.toInt) | |
} | |
implicit val LongReads = new Read[Long] { | |
def reads(s: String) = Try(s.toLong) | |
} | |
} | |
object Read extends ReadInstances { | |
def apply[T](f: String => Try[T]): Read[T] = new Read[T] { | |
override def reads(s: String) = f(s) | |
} | |
def reads[T](s: String)(implicit r: Read[T]): Try[T] = r reads s | |
} | |
@implicitNotFound(msg = "Implicit ToXml[${T}] not found. Try supplying an implicit instance of ToXml[${T}]") | |
trait ToXml[T] { | |
def toXml(t: T): NodeSeq | |
def toXml(t: T, rootLabel: String): NodeSeq = XML loadString s"<$rootLabel>${toXml(t)}</$rootLabel>" | |
} | |
@implicitNotFound(msg = "Implicit FroXml[${T}] not found. Try supplying an implicit instance of FromXml[${T}]") | |
trait FromXml[T] { | |
def fromXml(elem: NodeSeq): String \/ T | |
} | |
object ToXml extends LabelledProductTypeClassCompanion[ToXml] { | |
val Empty = NodeSeq.Empty | |
def apply[T](f: T => NodeSeq): ToXml[T] = new ToXml[T] { | |
override def toXml(t: T): NodeSeq = f(t) | |
} | |
def toXml[T](t: T)(implicit tx: ToXml[T]) = tx.toXml(t) | |
/*Anything that can be shown can be xml*/ | |
implicit def ShowsToXml[T: Show]: ToXml[T] = ToXml[T]{ t: T => | |
val s = Show[T].shows(t) | |
scala.xml.Text(s.stripPrefix("\"").stripSuffix("\"")) | |
} | |
implicit def OptionToXml[T](implicit tx: ToXml[T]) = new ToXml[Option[T]] { | |
override def toXml(o: Option[T]) = o.fold(Empty)(tx.toXml) | |
override def toXml(o: Option[T], rootLabel: String) = o.fold(Empty)(tx.toXml(_, rootLabel)) | |
} | |
/*Anything that is foldable can be serialized to xml*/ | |
implicit def FoldableToXml[T, L[_]](implicit tx: ToXml[T], foldable: Foldable[L]): ToXml[L[T]] = new ToXml[L[T]] { | |
override def toXml(list: L[T]): NodeSeq = list.foldLeft(Empty)(_ ++ tx.toXml(_)) | |
override def toXml(list: L[T], rootLabel: String): NodeSeq = list.foldLeft(Empty)(_ ++ tx.toXml(_, rootLabel)) | |
} | |
object typeClass extends LabelledProductTypeClass[ToXml] { | |
def emptyProduct: ToXml[HNil] = new ToXml[HNil] { | |
def toXml(t: HNil) = Empty | |
} | |
def product[F, T <: HList](name: String, FHead: ToXml[F], FTail: ToXml[T]) = new ToXml[F :#: T] { | |
override def toXml(hlist: F :#: T): NodeSeq = { | |
hlist match { | |
case head :#: tail => | |
val h = FHead toXml (head, name) | |
val t = FTail toXml tail | |
t ++ h | |
} | |
} | |
} | |
def project[F, G](instance: => ToXml[G], to: F => G, from: G => F) = new ToXml[F] { | |
override def toXml(f: F): NodeSeq = instance.toXml(to(f)) | |
} | |
} | |
} | |
object FromXml extends LabelledProductTypeClassCompanion[FromXml] { | |
/*Anything with a Read typeclass instance can be deserialized from xml*/ | |
implicit def ReadsFromXml[T](implicit read: Read[T]): FromXml[T] = FromXml[T]{ n: NodeSeq => | |
read.reads(n.text) match { | |
case Success(t) => t.right | |
case Failure(e) => s" [Error reading value: ${e.getMessage}]".left | |
} | |
} | |
implicit def OptionFromXml[T](implicit fx: FromXml[T]): FromXml[Option[T]] = FromXml[Option[T]] { node: NodeSeq => | |
node match { | |
case n if n.isEmpty => none.right | |
case n => fx.fromXml(n).map(_.some) | |
} | |
} | |
/*You know for like lists and sets and stuff*/ | |
implicit def MonoidFromXml[T, L[_]](implicit fx: FromXml[T], monoid: Monoid[L[T]], app: Applicative[L]): FromXml[L[T]] = FromXml[L[T]]{ node: NodeSeq => | |
val zero = monoid.zero.right[String] | |
node.foldLeft(zero){ (listF, n) => | |
for { | |
list <- listF | |
f <- fx fromXml n | |
v = app point f | |
} yield monoid.append(list, v) | |
} | |
} | |
def apply[T](f: NodeSeq => String \/ T): FromXml[T] = new FromXml[T] { | |
def fromXml(t: NodeSeq) = f(t) | |
} | |
object typeClass extends LabelledProductTypeClass[FromXml] { | |
def emptyProduct: FromXml[HNil] = new FromXml[HNil] { | |
def fromXml(t: NodeSeq) = HNil.right | |
} | |
def product[F, T <: HList](name: String, FHead: FromXml[F], FTail: FromXml[T]) = new FromXml[F :#: T] { | |
override def fromXml(v: NodeSeq) = { | |
val node = (v \ name) | |
val result = for { | |
h <- FHead fromXml node | |
t <- FTail fromXml (v diff node) | |
} yield h :: t | |
result.leftMap(msg => s"/$name" + msg) | |
} | |
} | |
def project[F, G](instance: => FromXml[G], to: F => G, from: G => F) = new FromXml[F] { | |
override def fromXml(f: NodeSeq) = instance.fromXml(f).map(from) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@negator nice! I was doing some research on shapeless, ended up running into this.