app/models/Attempt.scala (111 lines of code) (raw):
package models
import play.api.mvc.Result
import scala.concurrent.{ExecutionContext, Future}
case class Attempt[A] private (underlying: Future[Either[AMIableErrors, A]]) {
def map[B](f: A => B)(implicit ec: ExecutionContext): Attempt[B] =
flatMap(a => Attempt.Right(f(a)))
def flatMap[B](
f: A => Attempt[B]
)(implicit ec: ExecutionContext): Attempt[B] = Attempt {
asFuture.flatMap {
case Right(a) => f(a).asFuture
case Left(e) => Future.successful(Left(e))
}
}
def fold[B](failure: AMIableErrors => B, success: A => B)(implicit
ec: ExecutionContext
): Future[B] = {
asFuture.map(_.fold(failure, success))
}
def map2[B, C](
bAttempt: Attempt[B]
)(f: (A, B) => C)(implicit ec: ExecutionContext): Attempt[C] = {
for {
a <- this
b <- bAttempt
} yield f(a, b)
}
/** If there is an error in the Future itself (e.g. a timeout) we convert it
* to a Left so we have a consistent error representation. This would likely
* have logging around it, or you may have an error representation that
* carries more info for these kinds of issues.
*/
def asFuture(implicit
ec: ExecutionContext
): Future[Either[AMIableErrors, A]] = {
underlying recover { case err =>
val apiErrors =
AMIableErrors(AMIableError(err.getMessage, "Unexpected error", 500))
scala.Left(apiErrors)
}
}
}
object Attempt {
/** As with `Future.sequence`, changes `List[Attempt[A]]` to
* `Attempt[List[A]]`.
*
* This implementation returns the first failure in the list, or the
* successful result.
*/
def sequence[A](
responses: List[Attempt[A]]
)(implicit ec: ExecutionContext): Attempt[List[A]] = {
traverse(responses)(identity)
}
/** Changes generated `List[Attempt[A]]` to `Attempt[List[A]]` via provided
* function (like `Future.traverse`).
*
* This implementation returns the first failure in the list, or the
* successful result.
*/
def traverse[A, B](
as: List[A]
)(f: A => Attempt[B])(implicit ec: ExecutionContext): Attempt[List[B]] = {
as.foldRight[Attempt[List[B]]](Right(Nil))(f(_).map2(_)(_ :: _))
}
/** Sequence this attempt as a successful attempt that contains a list of
* potential failures. This is useful if failure is acceptable in part of the
* application.
*/
def sequenceFutures[A](
response: List[Attempt[A]]
)(implicit ec: ExecutionContext): Attempt[List[Either[AMIableErrors, A]]] = {
Async.Right(Future.traverse(response)(_.asFuture))
}
def fromEither[A](e: Either[AMIableErrors, A]): Attempt[A] =
Attempt(Future.successful(e))
def fromOption[A](optA: Option[A], ifNone: AMIableErrors): Attempt[A] =
fromEither(optA.toRight(ifNone))
/** Convert a plain `Future` value to an attempt by providing a recovery
* handler.
*/
def fromFuture[A](future: Future[Either[AMIableErrors, A]])(
recovery: PartialFunction[Throwable, Either[AMIableErrors, A]]
)(implicit ec: ExecutionContext): Attempt[A] = {
Attempt(future recover recovery)
}
def future[A](future: Future[A])(
recovery: PartialFunction[Throwable, Either[AMIableErrors, A]]
)(implicit ec: ExecutionContext): Attempt[A] = {
Attempt(future.map(scala.Right(_)) recover recovery)
}
/** Discard failures from a list of attempts.
*
* **Use with caution**.
*/
def successfulAttempts[A](
attempts: List[Attempt[A]]
)(implicit ec: ExecutionContext): Attempt[List[A]] = {
Attempt.Async.Right {
Future
.traverse(attempts)(_.asFuture)
.map(_.collect { case Right(a) => a })
}
}
/** Create an Attempt instance from a "good" value.
*/
def Right[A](a: A): Attempt[A] =
Attempt(Future.successful(scala.Right(a)))
/** Create an Attempt failure from an AMIableErrors instance, representing the
* possibility of multiple failures.
*/
def Left[A](errs: AMIableErrors): Attempt[A] =
Attempt(Future.successful(scala.Left(errs)))
/** Create an Attempt failure if there's only a single error.
*/
def Left[A](err: AMIableError): Attempt[A] =
Attempt(Future.successful(scala.Left(AMIableErrors(err))))
/** Asyncronous versions of the Attempt Right/Left helpers for when you have a
* Future that returns a good/bad value directly.
*/
object Async {
/** Create an Attempt from a Future of a good value.
*/
def Right[A](fa: Future[A])(implicit ec: ExecutionContext): Attempt[A] =
Attempt(fa.map(scala.Right(_)))
/** Create an Attempt from a known failure in the future. For example, if a
* piece of logic fails but you need to make a Database/API call to get the
* failure information.
*/
def Left[A](ferr: Future[AMIableErrors])(implicit
ec: ExecutionContext
): Attempt[A] =
Attempt(ferr.map(scala.Left(_)))
}
def apply[A](
action: => Attempt[Result]
)(errorHandler: AMIableErrors => Result)(implicit ec: ExecutionContext) = {
action.fold(
errorHandler,
identity
)
}
}
case class AMIableErrors(errors: List[AMIableError]) {
def statusCode = errors.map(_.statusCode).max
def logString = errors.map(_.message).mkString(", ")
}
object AMIableErrors {
def apply(error: AMIableError): AMIableErrors = {
AMIableErrors(List(error))
}
def apply(errors: Seq[AMIableError]): AMIableErrors = {
AMIableErrors(errors.toList)
}
}