Skip to content

Instantly share code, notes, and snippets.

@mrange
Last active October 22, 2017 09:14
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 mrange/f45c4bb4ac352a7aebfc884759085b75 to your computer and use it in GitHub Desktop.
Save mrange/f45c4bb4ac352a7aebfc884759085b75 to your computer and use it in GitHub Desktop.
import ScalaTransformer.AnyTransformer
object ScalaTransformer {
import scala.collection.mutable.ArrayBuffer
sealed abstract class PathElement
object PathElement {
case class Property (name : String) extends PathElement
case class Index (index: Int) extends PathElement
case class Named (name : String) extends PathElement
}
type Path = List[PathElement]
sealed abstract class AnyTransformerFailure
object AnyTransformerFailure {
case class Exception (throwable: Throwable) extends AnyTransformerFailure
case class Message (message : String) extends AnyTransformerFailure
case class MissingMember (member : String) extends AnyTransformerFailure
case class IndexOutOfRange(index : Int) extends AnyTransformerFailure
case class NotAMap () extends AnyTransformerFailure
case class NotIndexable () extends AnyTransformerFailure
}
sealed abstract class AnyTransformerTree
object AnyTransformerTree {
case class Empty () extends AnyTransformerTree
case class Leaf (path: Path, failure: AnyTransformerFailure) extends AnyTransformerTree
case class Suppress (tree: AnyTransformerTree) extends AnyTransformerTree
case class Fork (left: AnyTransformerTree, right: AnyTransformerTree) extends AnyTransformerTree
val empty = Empty()
def flatten(t: AnyTransformerTree): (Array[(String,AnyTransformerFailure)], Array[(String,AnyTransformerFailure)]) = {
val es = new ArrayBuffer[(String,AnyTransformerFailure)]
val ws = new ArrayBuffer[(String,AnyTransformerFailure)]
val sb = new StringBuilder()
def path(p: Path): Unit = p match {
case head::tail =>
path(tail)
head match {
case p : PathElement.Property => sb.append('.').append(p.name)
case i : PathElement.Index => sb.append('[').append(i.index).append(']')
case n : PathElement.Named => sb.append('(').append(n.name).append(')')
}
case Nil =>
sb.setLength(0)
sb.append("root")
}
def loop(suppress: Boolean, t: AnyTransformerTree): Unit = t match {
case _ : Empty =>
()
case l : Leaf =>
path(l.path)
val ss = if(suppress) ws else es
val p = (sb.toString, l.failure)
ss += p
case s : Suppress =>
loop(suppress = true, s.tree)
case f : Fork =>
loop(suppress, f.left)
loop(suppress, f.right)
}
loop(suppress = false, t)
(es.toArray, ws.toArray)
}
def leaf(p: Path)(f: AnyTransformerFailure) = Leaf(p, f)
def suppress(t: AnyTransformerTree): AnyTransformerTree = {
t match {
case _ : Empty => t
case _ : Suppress => t
case _ => Suppress(t)
}
}
def fork(l: AnyTransformerTree, r: AnyTransformerTree): AnyTransformerTree = {
(l, r) match {
case (_: Empty , _ ) => r
case (_ , _: Empty ) => l
case (l: Suppress , r: Suppress ) => Suppress(Fork(l, r))
case _ => Fork(l, r)
}
}
def isGood(t: AnyTransformerTree): Boolean = {
t match {
case _ : Empty => true
case _ : Suppress => true
case _ => false
}
}
}
sealed abstract class AnyTransformerRun[+A]
object AnyTransformerRun {
private def appendTo(sb: StringBuilder, vs: Array[(String,AnyTransformerFailure)]) = {
for ((path, failure) <- vs) {
sb.append(", ")
sb.append(path)
sb.append("->")
sb.append(failure)
}
}
case class IsGood [A] (v: A) extends AnyTransformerRun[A]
case class WithWarnings [A] (v: A, warnings: Array[(String,AnyTransformerFailure)]) extends AnyTransformerRun[A] {
override def toString: String = {
val sb = new StringBuilder()
sb.append(s"$v with suppressed")
AnyTransformerRun.appendTo(sb, warnings)
sb.toString
}
}
case class WithErrors [A] (v: A, warnings: Array[(String,AnyTransformerFailure)], errors: Array[(String,AnyTransformerFailure)]) extends AnyTransformerRun[A] {
override def toString: String = {
val sb = new StringBuilder()
sb.append(s"$v with errors")
AnyTransformerRun.appendTo(sb, errors)
if (warnings.length > 0) {
sb.append(" with suppressed")
AnyTransformerRun.appendTo(sb, warnings)
}
sb.toString
}
}
}
case class AnyTransformerResult[+A](value: A, tree: AnyTransformerTree)
object AnyTransformerResult {
@inline def result[A](v: A, t: AnyTransformerTree): AnyTransformerResult[A] = AnyTransformerResult[A](v, t)
@inline def good [A](v: A) : AnyTransformerResult[A] = result(v, AnyTransformerTree.empty)
}
// TODO: Make AnyTransformer covariant (conflicts with <|>)
case class AnyTransformer[+A](f: (Any, Path) => AnyTransformerResult[A]) {
def >>=[B](bf: A => AnyTransformer[B]): AnyTransformer[B] = AnyTransformer.bind(this)(bf)
def |>>[B](m: A => B) : AnyTransformer[B] = AnyTransformer.map(m)(this)
def <&>[B](b: AnyTransformer[B]) : AnyTransformer[(A, B)] = AnyTransformer.and(this)(b)
// def <|> (b: AnyTransformer[A]) : AnyTransformer[A] = AnyTransformer.or(this)(b)
}
object AnyTransformer {
import AnyTransformerResult._
import AnyTransformerTree._
// Monad
def unit[A](a: A): AnyTransformer[A] =
AnyTransformer((v, p) => good(a))
def bind[A,B](a: AnyTransformer[A])(bf: A => AnyTransformer[B]): AnyTransformer[B] =
AnyTransformer((v, p) => {
val ar = a.f(v, p)
val b = bf(ar.value)
val br = b.f(v, p)
result(br.value, fork(ar.tree, br.tree))
})
def bindIf[A,B](a: AnyTransformer[A])(fallback: B)(bf: A => AnyTransformer[B]): AnyTransformer[B] =
AnyTransformer((v, p) => {
val ar = a.f(v, p)
if (isGood(ar.tree)) {
val b = bf(ar.value)
val br = b.f(v, p)
result(br.value, fork(ar.tree, br.tree))
} else {
result(fallback, ar.tree)
}
})
// Applicative
def ap[A,B](f: AnyTransformer[A => B])(a: AnyTransformer[A]): AnyTransformer[B] =
AnyTransformer((v, p) => {
val fr = f.f(v, p)
val ar = a.f(v, p)
result(fr.value(ar.value), fork(fr.tree, ar.tree))
})
// Functor
def map[A,B](m: A => B)(a: AnyTransformer[A]): AnyTransformer[B] =
AnyTransformer((v, p) => {
val ar = a.f(v, p)
result(m(ar.value), ar.tree)
})
// Combinators
def and[A,B](a: AnyTransformer[A])(b: AnyTransformer[B]): AnyTransformer[(A, B)] =
AnyTransformer((v, p) => {
val ar = a.f(v, p)
val br = b.f(v, p)
result((ar.value, br.value), fork(ar.tree, br.tree))
})
def or[A](a: AnyTransformer[A])(b: AnyTransformer[A]): AnyTransformer[A] =
AnyTransformer((v, p) => {
val ar = a.f(v, p)
val br = b.f(v, p)
if (isGood(ar.tree)) {
result(ar.value, fork(ar.tree, suppress(br.tree)))
} else if (isGood(br.tree)) {
result(br.value, fork(suppress(ar.tree), br.tree))
} else {
result(ar.value, fork(ar.tree, br.tree))
}
})
def pickFirst[A](first: AnyTransformer[A], rest: AnyTransformer[A]*): AnyTransformer[A] =
AnyTransformer((v, p) => {
def loop(i: Int, fallback: A, tree: AnyTransformerTree): AnyTransformerResult[A] = {
if (i < rest.length) {
val a = rest(i)
val ar = a.f(v, p)
if (isGood(ar.tree)) {
result(ar.value, ar.tree)
} else {
loop(i + 1, fallback, fork(tree, ar.tree))
}
} else {
result(fallback, tree)
}
}
val ar = first.f(v, p)
if (isGood(ar.tree)) {
result(ar.value, ar.tree)
} else {
loop(0, ar.value, ar.tree)
}
})
// Misc
def debug[A](name: String)(a: AnyTransformer[A]): AnyTransformer[A] =
AnyTransformer((v, p) => {
println(s"$name - BEFORE - $p - $v")
val ar = a.f(v, p)
println(s"$name - AFTER - $p - $v - $ar")
ar
})
def optional[A](a: AnyTransformer[A]): AnyTransformer[Option[A]] =
AnyTransformer((v, p) => {
val ar = a.f(v, p)
if(isGood(ar.tree)) {
result(Some(ar.value), ar.tree)
} else {
result(None, suppress(ar.tree))
}
})
def verify[A](vf: A => Option[String])(a: AnyTransformer[A]): AnyTransformer[A] =
AnyTransformer((v, p) => {
val ar = a.f(v, p)
vf(ar.value) match {
case Some(msg) =>
result(ar.value, fork(ar.tree, leaf(p)(AnyTransformerFailure.Message(msg))))
case None =>
ar
}
})
def suppressWith[A](value: A)(a: AnyTransformer[A]): AnyTransformer[A] =
AnyTransformer((v, p) => {
val ar = a.f(v, p)
if(isGood(ar.tree)) {
result(ar.value, ar.tree)
} else {
result(value, suppress(ar.tree))
}
})
def run[A](a: AnyTransformer[A])(v: Any): AnyTransformerRun[A] = {
val ar = a.f(v, List.empty)
val (es, ws) = flatten(ar.tree)
if (es.length == 0 && ws.length == 0) {
AnyTransformerRun.IsGood(ar.value)
} else if (es.length == 0) {
AnyTransformerRun.WithWarnings(ar.value, ws)
} else {
AnyTransformerRun.WithErrors(ar.value, ws, es)
}
}
// Extractors
def asString: AnyTransformer[String] =
AnyTransformer((v, p) => {
good(if (v != null) v.toString else "")
})
def asInt: AnyTransformer[Int] =
AnyTransformer((v, p) => {
if(v != null) {
// TODO: Don't use exceptions
try {
good(Integer.parseInt(v.toString))
} catch {
case _: Throwable => result(0, leaf(p)(AnyTransformerFailure.Message("Can't interpret value as integer")))
}
} else {
good(0)
}
})
// Navigators
def member[A](name: String)(fallback: A)(t : AnyTransformer[A]): AnyTransformer[A] =
AnyTransformer((v, p) => {
v match {
case m : Map[String, Any] =>
m.get(name) match {
case Some(v) =>
val pp = PathElement.Property(name)::p
t.f(v, pp)
case None => result(fallback, leaf(p)(AnyTransformerFailure.MissingMember(name)))
}
case _ => result(fallback, leaf(p)(AnyTransformerFailure.NotAMap()))
}
})
def named[A](name: String)(t : AnyTransformer[A]): AnyTransformer[A] =
AnyTransformer((v, p) => {
val pp = PathElement.Named(name)::p
t.f(v, pp)
})
def index[A](index: Int)(fallback: A)(t : AnyTransformer[A]): AnyTransformer[A] =
AnyTransformer((v, p) => {
v match {
case a : Array[Any] =>
if (index >= 0 && index < a.length) {
val pp = PathElement.Index(index)::p
t.f(a(index), pp)
} else {
result(fallback, leaf(p)(AnyTransformerFailure.IndexOutOfRange(index)))
}
case _ => result(fallback, leaf(p)(AnyTransformerFailure.NotIndexable()))
}
})
}
implicit class ApAnyTransformer[A,B](f: AnyTransformer[A => B]) {
def <*>(a: AnyTransformer[A]): AnyTransformer[B] = AnyTransformer.ap(f)(a)
}
}
abstract class Request
object Request {
case class Customer(firstName: String, lastName: String, nationalId: String) extends Request
case class Employee(firstName: String, lastName: String, employeeId: String) extends Request
case class Mvp (firstName: String, lastName: String, mvpId : String) extends Request
val empty: Request = Customer("", "", "")
}
case class Envelope(envelopeId: String, request: Request)
object Main extends App {
val empty : Map[String, Any] = Map.empty
val customer = empty
.updated("envelopeId", "EID_1234")
.updated("request" , empty
.updated("@schema" , "customer" )
.updated("nationalId" , "12345" )
.updated("firstName" , "Bill" )
.updated("lastName" , "Gates" )
)
val employee = empty
.updated("envelopeId", "EID_1234")
.updated("request" , empty
.updated("@schema" , "employee" )
.updated("employeeId" , "1" )
.updated("firstName" , "Bill" )
.updated("lastName" , "Gates" )
)
val mvp = empty
.updated("envelopeId", "EID_1234")
.updated("request" , empty
.updated("@schema" , "mvp" )
.updated("mvpId" , "9480" )
.updated("firstName" , "Bill" )
.updated("lastName" , "Gates" )
)
val busted = empty
// .updated("envelopeId", "EID_1234")
.updated("request" , empty
.updated("@schema" , "mvp" )
// .updated("mvpId" , "9480" )
.updated("firstName" , "Bill" )
.updated("lastName" , "Gates" )
)
import ScalaTransformer.AnyTransformer._
def str(name: String) = member(name)("")(asString)
def schema[A](schema: String)(a: AnyTransformer[Request]) = {
val msg = Some(s"@schema expected to be: $schema")
val v = member("@schema")("")(verify[String](s => if (s == schema) None else msg)(asString))
bindIf(v)(Request.empty)(_ => a)
}
val tcustomer =
schema("customer")(
unit(Request.Customer.curried) <*>
str("firstName") <*>
str("lastName") <*>
str("nationalId") )
val temployee =
schema("employee")(
unit(Request.Employee.curried) <*>
str("firstName") <*>
str("lastName") <*>
str("employeeId") )
val tmvp =
schema("mvp")(
unit(Request.Mvp.curried) <*>
str("firstName") <*>
str("lastName") <*>
str("mvpId") )
val trequest = pickFirst(tcustomer, temployee, tmvp)
val tenvelope =
unit(Envelope.curried) <*>
str("envelopeId") <*>
member("request")(Request.empty)(trequest)
val r = run(tenvelope)(busted)
println(s"Result: $r")
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment