aboutsummaryrefslogblamecommitdiff
path: root/src/main/scala/xyz/driver/pdsuicommon/computation/Computation.scala
blob: a9430e36f74e759e7f993f2922d57e7bfc67dc66 (plain) (tree)
1
                                          
















                                                                                                    
                                                         




























                                                                                                                        
                                                 



                                  

                                                                                                            

                                                          
                              








                                                                                             






                                                                                    







                                                                                                       













                                                                                               
                    


                                                                                                                    
                      



                                                                                                          
                      

                      










                                                                                                             


                    
   
 
package xyz.driver.pdsuicommon.computation

import scala.concurrent.{ExecutionContext, Future}

/**
  * Takes care of computations
  *
  * Success(either) - the computation will be continued.
  * Failure(error) - the computation was failed with unhandled error.
  *
  * Either[Result, T]:
  * Left(result) is a final and handled result, another computations (map, flatMap) will be ignored.
  * Right(T) is a current result. Functions in map/flatMap will continue the computation.
  *
  * Example:
  * {{{
  * import scala.concurrent.ExecutionContext.Implicits.global
  * import scala.concurrent.{ExecutionContext, Future}
  * import xyz.driver.pdsuicommon.computation.Computation
  *
  * def successful = for {
  *   x <- Computation.continue(1)
  *   y <- Computation.continue(2)
  * } yield s"\$x + \$y"
  *
  * // Prints "Success(1 + 2)"
  * successful.join.onComplete(print)
  *
  * def failed = for {
  *   x <- Computation.abort("Failed on x")
  *   _ = print("Second step")
  *   y <- Computation.continue(2)
  * } yield s"\$x + \$y"
  *
  * // Prints "Success(Failed on x)"
  * failed.join.onComplete(print)
  * }}}
  *
  * TODO: Make future private
  *
  * @param future The final flow in a future.
  * @tparam R Type of result for aborted computation.
  * @tparam T Type of result for continued computation.
  */
final case class Computation[+R, +T](future: Future[Either[R, T]]) {

  def flatMap[R2, T2](f: T => Computation[R2, T2])(implicit ec: ExecutionContext, ev: R <:< R2): Computation[R2, T2] = {
    Computation(future.flatMap {
      case Left(x)  => Future.successful(Left(x))
      case Right(x) => f(x).future
    })
  }

  def processExceptions[R2](f: PartialFunction[Throwable, R2])(implicit ev1: R <:< R2,
                                                               ec: ExecutionContext): Computation[R2, T] = {
    val strategy = f.andThen(x => Left(x): Either[R2, T])
    val castedFuture: Future[Either[R2, T]] = future.map {
      case Left(x)  => Left(x)
      case Right(x) => Right(x)
    }
    Computation(castedFuture.recover(strategy))
  }

  def map[T2](f: T => T2)(implicit ec: ExecutionContext): Computation[R, T2] = flatMap { a =>
    Computation.continue(f(a))
  }

  def mapLeft[R2](f: R => R2)(implicit ec: ExecutionContext): Computation[R2, T] = {
    Computation(future.map {
      case Left(x)  => Left(f(x))
      case Right(x) => Right(x)
    })
  }

  def mapAll[R2, T2](onLeft: R => Computation[R2, T2])(onRight: T => Computation[R2, T2])(
          onFailure: () => Computation[R2, T2])(implicit ec: ExecutionContext): Computation[R2, T2] = {

    Computation(future.flatMap(_.fold(onLeft, onRight).future).recoverWith {
      case _ => onFailure().future
    })
  }

  def andThen(f: T => Any)(implicit ec: ExecutionContext): Computation[R, T] = map { a =>
    f(a)
    a
  }

  def filter(f: T => Boolean)(implicit ec: ExecutionContext): Computation[R, T] = map { a =>
    if (f(a)) a
    else throw new NoSuchElementException("When filtering")
  }

  def withFilter(f: T => Boolean)(implicit ec: ExecutionContext): Computation[R, T] = filter(f)

  def foreach[T2](f: T => T2)(implicit ec: ExecutionContext): Unit = future.foreach {
    case Right(x) => f(x)
    case _        =>
  }

  def toFuture[R2](resultFormatter: T => R2)(implicit ec: ExecutionContext, ev: R <:< R2): Future[R2] = future.map {
    case Left(x)  => x
    case Right(x) => resultFormatter(x)
  }

  def toFuture[R2](implicit ec: ExecutionContext, ev1: R <:< R2, ev2: T <:< R2): Future[R2] = future.map {
    case Left(x)  => x
    case Right(x) => x
  }
}

object Computation {

  def continue[T](x: T): Computation[Nothing, T] = Computation(Future.successful(Right(x)))

  def abort[R](result: R): Computation[R, Nothing] = Computation(Future.successful(Left(result)))

  def fail(exception: Throwable): Computation[Nothing, Nothing] = Computation(Future.failed(exception))

  def fromFuture[T](input: Future[T])(implicit ec: ExecutionContext): Computation[Nothing, T] = Computation {
    input.map { x =>
      Right(x)
    }
  }
}