aboutsummaryrefslogtreecommitdiff
path: root/src/main/scala/xyz/driver/pdsuicommon/computation/Computation.scala
blob: a9430e36f74e759e7f993f2922d57e7bfc67dc66 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
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)
    }
  }
}