app/models/Attempt.scala (92 lines of code) (raw):

package models import org.slf4j.Logger import play.api.mvc.Result import scala.concurrent.{ExecutionContext, Future} case class Attempt[A] private (underlying: Future[Either[AttemptErrors, 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 recoverWith(f: AttemptErrors => Attempt[A])(implicit ec: ExecutionContext): Attempt[A] = Attempt { asFuture.flatMap { case Left(e) => f(e).asFuture case Right(a) => Future.successful(Right(a)) } } def fold[B](failure: AttemptErrors => B, success: A => B)(implicit ec: ExecutionContext): Future[B] = { asFuture.map(_.fold(failure, success)) } def foreach(f: A => Unit)(implicit ec: ExecutionContext): Unit = map(f) /** * 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[AttemptErrors, A]] = { underlying recover { case err => val apiErrors = AttemptErrors(AttemptError(err.getMessage, throwable = Some(err))) scala.Left(apiErrors) } } } object Attempt { /** * As with `Future.sequence`, changes `List[Attempt[A]]` to `Attempt[List[A]]`. * * This implementation takes the first failure for simplicity, it's possible * to collect all the failures when that's required. */ def sequence[A](responses: Seq[Attempt[A]])(implicit ec: ExecutionContext): Attempt[Seq[A]] = Attempt { Future.sequence(responses.map(_.underlying)).map { eithers => eithers .collectFirst { case scala.Left(x) => scala.Left(x): Either[AttemptErrors, Seq[A]]} .getOrElse { scala.Right(eithers collect { case Right(x) => x}) } } } /** * 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[AttemptErrors, A]]] = { Async.Right(Future.sequence(response.map(_.asFuture))) } def fromEither[A](e: Either[AttemptErrors, A]): Attempt[A] = Attempt(Future.successful(e)) /** * Convert a plain `Future` value to an attempt by providing a recovery handler. */ def fromFuture[A](future: Future[Either[AttemptErrors, A]])(recovery: PartialFunction[Throwable, Either[AttemptErrors, A]])(implicit ec: ExecutionContext): Attempt[A] = { Attempt(future recover recovery) } /** * Discard failures from a list of attempts. * * **Use with caution**. */ def successfulAttempts[A](attempts: Seq[Attempt[A]])(implicit ec: ExecutionContext): Attempt[Seq[A]] = { Attempt.Async.Right { Future.sequence(attempts.map { attempt => attempt.fold(_ => None, a => Some(a)) }).map(_.flatten) } } /** * 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. */ def Left[A](err: AttemptErrors): Attempt[A] = Attempt(Future.successful(scala.Left(err))) def Left[A](err: AttemptError): Attempt[A] = Attempt(Future.successful(scala.Left(AttemptErrors(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[AttemptErrors])(implicit ec: ExecutionContext): Attempt[A] = Attempt(ferr.map(scala.Left(_))) } def apply[A](action: => Attempt[Result])(errorHandler: AttemptErrors => Result)(implicit ec: ExecutionContext) = { action.fold( errorHandler, identity ) } } case class AttemptError(message: String, context: Option[String] = None, throwable: Option[Throwable] = None) { lazy val logString = s"${context.fold("")(_+": ")}$message" def logTo(logger: Logger): Unit = { throwable match { case None => logger.error(logString) case Some(t) => logger.error(logString, t) } } } case class AttemptErrors(errors: List[AttemptError]) object AttemptErrors { def apply(error: AttemptError): AttemptErrors = { AttemptErrors(List(error)) } def apply(errors: Seq[AttemptError]): AttemptErrors = { AttemptErrors(errors.toList) } }