Last active
October 22, 2017 09:14
-
-
Save mrange/f45c4bb4ac352a7aebfc884759085b75 to your computer and use it in GitHub Desktop.
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
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