aboutsummaryrefslogtreecommitdiff
path: root/src/main/scala/xyz/driver/pdsuicommon/computation/Computation.scala
blob: af7d05129f8d677a39092c3a8186cd06ba9e7308 (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
125
126
127
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 { success =>
      if (success.isRight) onRight(success.right.get).future
      else onLeft(success.left.get).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)
    }
  }
}