Appearance
Asyncronus Design
Vasat is designed to take full advantage of asyncronus patterns.
The majority of the methods used in Vasat return a scala Future
Error handling in vasat also leverages the Cats Validated
object rather than just relying on throwing exceptions. This allows for aggreation of multiple errors.
An Intro into VValids
In Scala you can Have an Either[A,B]
scala
val luckDip:Either[Bike,Car] = ???
luckyDip match {
case Right(a:ToyotaCamry) =>
case Right(a:MazdaRx8) =>
case Right(a) => // some other extends car
case Left(a:HarleyDavidson) =>
case Left(a:BMXBike) =>
case Left(a) => // a must be a Bike
}
Unlike the example above, the majority of the time an either was used to return either a good or a bad result
Cats improved on this calling it a Validated[A,B]
A Validated[A,B], can Either be an Invalid(obj:A) or a Valid(obj:B) this is a slight improvement from the scala Either which is a Left(obj:A) or a Right(obj:B). With later versions of scala an Eithers a 'Left' is supposed to be the wrong or bad outcome. This wasn't the best terminology
For an invalid type, the generic that would make sense would be a Throwable. If we add the concept of a cats invalidNel which is a NonEmptyList we could say the Invalid response is a list of Throwables with at least 1 throwable (ie You cant have a Invalid[EmptyList])
Make the type of the non empty list a Vasat Specific Exception type 'VError' we have a VValid[A]
A
VValid[A]
is the same asValidated[NonEmptyList[VError],A]
that means when its valid, its whatever you want the value to be, when its invalid its 1 or more VError objects.
A Cats Validated can have a .map( ) method that means if its valid, map to a valid of the following type
eg:
scala
val v = Valid("hello").map(h => h.toUpperCase())
// Valid("HELLO")
if you want the valid object, but you want another bit of logic that might be invalid
scala
val v = Valid("hello").andThen(h =>
if(h == "hello")
Valid(1234)
else
InvalidNel(VError("not hello!"))
)
// VValid[Int] Either Valid(1234), or Invalid(NonEmptyList(Verror("not hello!')))
The use of VValids over just throwing exceptions allows for errors to be collected and code to possibly move on beyond the first Invalid
eg:
scala
checkName(u)
checkPassword(u)
checkEmail(u)
If all 3 name email and passowrd were wrong for a user, checkName would throw an exception and you would know name was wrong, only to re-submit to find out password was wrong, only to re-submit to find out email was wrong
With Valids and Cats:
scala
def checkName(u:User):VValid[Noop] = ???
def checkPassword(u:User):VValid[Noop] = ???
def checkEmail(u:User):VValid[Noop] = ???
(checkEmail(u),checkPassword(u),checkEmail(u)).mapN((a,b,c) => "OK")
mapN is a cats function that will add NonEmptyLists of invalids, so in this case if there were 3 errors you would get a result Invalid(NonEmptyList(error1,List(erro2,errro3))
A word on Noop
Sometimes the only purpose of a Validated is to give the error if there is one, a No-operation or Noop means "I dont care what the result is if it was successful".
An example would be on a save or update method.
Futures
Since most of vasat 2 is async its expected that results can be returned at some point in the future. And the Future values are for the most parts VValids
We have the shorthand type VValidF[A]
A
VValidF[A]
is aFuture[VValid[A]]
which is aFuture[Validaed[InvalidNel[VError],A]]
For operations when we want the future to return, and that return to be valid we have 3 shorthands
.andThenMap
When the future completes and is valid, take the result and map it to the following
scala
val v:VValidF[Int] = ???
v.andThenMap(r => r.toString)
// Future[VValid[String]]
.andThenVValid
When the future completes and is valid, take the result and the equate another VValid expression
scala
val v:VValidF[String] = ???
v.andThenVValid(r =>
if(h == "hello")
Valid(1234)
else
InvalidNel(VError("not hello!"))
)
)
// Future[VValid[Int]] aka VValidF[Int]
.andThenFuture
When the future completes and is valid, flatMap the result against another future that also returns a VValid. This is by far the most used shorthand as you may wish to load multiple things or or load, and save and when all of that is done in a correct order, return a valid
scala
val v:VValidF[String] = ???
v.andThenFuture(r =>
val v2:VValidF[Long] = ???
v2
)
// VValidF[Long]]
An example
scala
val res = myRepo.readObject("Abc").andThenFuture{obj =>
val modifyed = obj.copy(name = "blah")
myRepo.writeObject("Abc",modifiyed)
}.andThenMap{_ =>
"Success"
}
// res == VValidF[String]
VValidFs and play
A VValid and VValidF have a json serializer what will render the result as the familiar vasat json payload response. There are some play helpers to make this happen
scala
import vasat.play.helpers.PlayHelpers._
val res = myRepo.readObject("Abc").andThenFuture{obj =>
val modifyed = obj.copy(name = "blah")
myRepo.writeObject("Abc",modifiyed)
}.andThenMap{_ =>
"Success"
}.toResult
will look like
scala
{
status:200,
payload:"Success",
message:"OK",
time:xxxxxx
}
or
scala
{
status:<verror code>
error:{
// json of a verror
},
message:<verror message>,
time:xxxxxx
}
VValidFs and Akka Streams
A very common case is when we want to run a list of operations. For example getting a list obj Accounts and setting them to inactive. the method .mapAsync is used to construct an Akka Source / Sink and run the items asyncrounsly
scala
import vasat.helpers.CatsHelpers._
val items:List[Ref[Account]] = ???
val res = items.mapAsync{a =>
val modifyed = a.item.copy(active = false)
// map the result to be the ID of this item
myRepo.writeObject(a.id,modifiyed).andThenMap{_ => a.id}
}
// res == VValidF[List[String]]
It is important to note that with this operator, if theres more than 1 invalid entry, the Invalid NonEmptryList will return all the errors