All Downloads are FREE. Search and download functionalities are using the official Maven repository.

zhttp.http.Middleware.scala Maven / Gradle / Ivy

There is a newer version: 2.0.0-RC11
Show newest version
package zhttp.http

import zhttp.http.middleware.Web
import zio.{Duration, ZIO}

/**
 * Middlewares are essentially transformations that one can apply on any Http to
 * produce a new one. They can modify requests and responses and also transform
 * them into more concrete domain entities.
 *
 * You can think of middlewares as a functions —
 *
 * {{{
 *   type Middleware[R, E, AIn, BIn, AOut, BOut] = Http[R, E, AIn, BIn] => Http[R, E, AOut, BOut]
 * }}}
 *
 * The `AIn` and `BIn` type params represent the type params of the input Http.
 * The `AOut` and `BOut` type params represent the type params of the output
 * Http.
 */
trait Middleware[-R, +E, +AIn, -BIn, -AOut, +BOut] { self =>

  /**
   * Applies middleware on Http and returns new Http.
   */
  def apply[R1 <: R, E1 >: E](http: Http[R1, E1, AIn, BIn]): Http[R1, E1, AOut, BOut]

  /**
   * Creates a new middleware that passes the output Http of the current
   * middleware as the input to the provided middleware.
   */
  final def >>>[R1 <: R, E1 >: E, AIn1 <: AOut, BIn1 >: BOut, AOut1, BOut1](
    other: Middleware[R1, E1, AIn1, BIn1, AOut1, BOut1],
  ): Middleware[R1, E1, AIn, BIn, AOut1, BOut1] = self andThen other

  /**
   * Applies self but if it fails, applies other.
   */
  final def <>[R1 <: R, E1, AIn0 >: AIn, BIn0 <: BIn, AOut0 <: AOut, BOut0 >: BOut](
    other: Middleware[R1, E1, AIn0, BIn0, AOut0, BOut0],
  ): Middleware[R1, E1, AIn0, BIn0, AOut0, BOut0] = self orElse other

  /**
   * Combines two middleware that don't modify the input and output types.
   */
  final def ++[R1 <: R, E1 >: E, A0 >: AIn <: AOut, B0 >: BOut <: BIn](
    other: Middleware[R1, E1, A0, B0, A0, B0],
  ): Middleware[R1, E1, A0, B0, A0, B0] =
    self combine other

  /**
   * Composes one middleware with another.
   */
  final def andThen[R1 <: R, E1 >: E, AIn1 <: AOut, BIn1 >: BOut, AOut1, BOut1](
    other: Middleware[R1, E1, AIn1, BIn1, AOut1, BOut1],
  ): Middleware[R1, E1, AIn, BIn, AOut1, BOut1] =
    new Middleware[R1, E1, AIn, BIn, AOut1, BOut1] {
      override def apply[R2 <: R1, E2 >: E1](http: Http[R2, E2, AIn, BIn]): Http[R2, E2, AOut1, BOut1] =
        other(self(http))
    }

  /**
   * Makes the middleware resolve with a constant Middleware
   */
  final def as[BOut0](
    bout: BOut0,
  ): Middleware[R, E, AIn, BIn, AOut, BOut0] =
    self.map(_ => bout)

  /**
   * Combines two middleware that operate on the same input and output types,
   * into one.
   */
  final def combine[R1 <: R, E1 >: E, A0 >: AIn <: AOut, B0 >: BOut <: BIn](
    other: Middleware[R1, E1, A0, B0, A0, B0],
  ): Middleware[R1, E1, A0, B0, A0, B0] =
    self andThen other

  /**
   * Preprocesses the incoming value for the outgoing Http.
   */
  final def contramap[AOut0](f: AOut0 => AOut): Middleware[R, E, AIn, BIn, AOut0, BOut] =
    self.contramapZIO[AOut0](a => ZIO.succeed(f(a)))

  /**
   * Preprocesses the incoming value using a ZIO, for the outgoing Http.
   */
  final def contramapZIO[AOut0]: Middleware.PartialContraMapZIO[R, E, AIn, BIn, AOut, BOut, AOut0] =
    new Middleware.PartialContraMapZIO(self)

  /**
   * Delays the production of Http output for the specified duration
   */
  final def delay(duration: Duration): Middleware[R, E, AIn, BIn, AOut, BOut] =
    self.mapZIO(b => ZIO.succeed(b).delay(duration))

  /**
   * Creates a new Middleware from another
   */
  final def flatMap[R1 <: R, E1 >: E, AIn0 >: AIn, BIn0 <: BIn, AOut0 <: AOut, BOut0](
    f: BOut => Middleware[R1, E1, AIn0, BIn0, AOut0, BOut0],
  ): Middleware[R1, E1, AIn0, BIn0, AOut0, BOut0] =
    new Middleware[R1, E1, AIn0, BIn0, AOut0, BOut0] {
      override def apply[R2 <: R1, E2 >: E1](http: Http[R2, E2, AIn0, BIn0]): Http[R2, E2, AOut0, BOut0] =
        self(http).flatMap(f(_)(http))
    }

  /**
   * Flattens an Middleware of a Middleware
   */
  final def flatten[R1 <: R, E1 >: E, AIn0 >: AIn, BIn0 <: BIn, AOut0 <: AOut, BOut0](implicit
    ev: BOut <:< Middleware[R1, E1, AIn0, BIn0, AOut0, BOut0],
  ): Middleware[R1, E1, AIn0, BIn0, AOut0, BOut0] =
    flatMap(identity(_))

  /**
   * Transforms the output type of the current middleware.
   */
  final def map[BOut0](f: BOut => BOut0): Middleware[R, E, AIn, BIn, AOut, BOut0] =
    self.flatMap(b => Middleware.succeed(f(b)))

  /**
   * Transforms the output type of the current middleware using effect function.
   */
  final def mapZIO[R1 <: R, E1 >: E, BOut0](f: BOut => ZIO[R1, E1, BOut0]): Middleware[R1, E1, AIn, BIn, AOut, BOut0] =
    self.flatMap(b => Middleware.fromHttp(Http.fromZIO(f(b))))

  /**
   * Applies self but if it fails, applies other.
   */
  final def orElse[R1 <: R, E1, AIn0 >: AIn, BIn0 <: BIn, AOut0 <: AOut, BOut0 >: BOut](
    other: Middleware[R1, E1, AIn0, BIn0, AOut0, BOut0],
  ): Middleware[R1, E1, AIn0, BIn0, AOut0, BOut0] =
    new Middleware[R1, E1, AIn0, BIn0, AOut0, BOut0] {
      override def apply[R2 <: R1, E2 >: E1](http: Http[R2, E2, AIn0, BIn0]): Http[R2, E2, AOut0, BOut0] =
        self(http) <> other(http)
    }

  /**
   * Race between current and other, cancels other when execution of one
   * completes
   */
  final def race[R1 <: R, E1 >: E, AIn1 >: AIn, BIn1 <: BIn, AOut1 <: AOut, BOut1 >: BOut](
    other: Middleware[R1, E1, AIn1, BIn1, AOut1, BOut1],
  ): Middleware[R1, E1, AIn1, BIn1, AOut1, BOut1] =
    new Middleware[R1, E1, AIn1, BIn1, AOut1, BOut1] {
      override def apply[R2 <: R1, E2 >: E1](http: Http[R2, E2, AIn1, BIn1]): Http[R2, E2, AOut1, BOut1] =
        self(http) race other(http)
    }

  final def runAfter[R1 <: R, E1 >: E](effect: ZIO[R1, E1, Any]): Middleware[R1, E1, AIn, BIn, AOut, BOut] =
    self.mapZIO(bOut => effect.as(bOut))

  final def runBefore[R1 <: R, E1 >: E](effect: ZIO[R1, E1, Any]): Middleware[R1, E1, AIn, BIn, AOut, BOut] =
    self.contramapZIO(b => effect.as(b))

  /**
   * Applies Middleware based only if the condition function evaluates to true
   */
  final def when[AOut0 <: AOut](cond: AOut0 => Boolean): Middleware[R, E, AIn, BIn, AOut0, BOut] =
    whenZIO(a => ZIO.succeed(cond(a)))

  /**
   * Applies Middleware based only if the condition effectful function evaluates
   * to true
   */
  final def whenZIO[R1 <: R, E1 >: E, AOut0 <: AOut](
    cond: AOut0 => ZIO[R1, E1, Boolean],
  ): Middleware[R1, E1, AIn, BIn, AOut0, BOut] =
    Middleware.ifThenElseZIO[AOut0](cond(_))(
      isTrue = _ => self,
      isFalse = _ => Middleware.identity,
    )
}

object Middleware extends Web {

  /**
   * Creates a middleware using the specified encoder and decoder functions
   */
  def codec[A, B]: PartialCodec[A, B] = new PartialCodec[A, B](())

  /**
   * Creates a codec middleware using two Http.
   */
  def codecHttp[A, B]: PartialCodecHttp[A, B] = new PartialCodecHttp[A, B](())

  /**
   * Creates a middleware using specified effectful encoder and decoder
   */
  def codecZIO[A, B]: PartialCodecZIO[A, B] = new PartialCodecZIO[A, B](())

  /**
   * Creates a middleware using specified function
   */
  def collect[A]: PartialCollect[A] = new PartialCollect[A](())

  /**
   * Creates a middleware using specified effect function
   */
  def collectZIO[A]: PartialCollectZIO[A] = new PartialCollectZIO[A](())

  /**
   * Creates a middleware which always fail with specified error
   */
  def fail[E](e: E): Middleware[Any, E, Nothing, Any, Any, Nothing] =
    new Middleware[Any, E, Nothing, Any, Any, Nothing] {
      override def apply[R1 <: Any, E1 >: E](http: Http[R1, E1, Nothing, Any]): Http[R1, E1, Any, Nothing] =
        Http.fail(e)
    }

  /**
   * Creates a middleware with specified http App
   */
  def fromHttp[R, E, A, B](http: Http[R, E, A, B]): Middleware[R, E, Nothing, Any, A, B] =
    new Middleware[R, E, Nothing, Any, A, B] {
      override def apply[R1 <: R, E1 >: E](other: Http[R1, E1, Nothing, Any]): Http[R1, E1, A, B] = http
    }

  /**
   * An empty middleware that doesn't do anything
   */
  def identity: Middleware[Any, Nothing, Nothing, Any, Any, Nothing] =
    new Middleware[Any, Nothing, Nothing, Any, Any, Nothing] {
      override def apply[R1 <: Any, E1 >: Nothing](http: Http[R1, E1, Nothing, Any]): Http[R1, E1, Any, Nothing] =
        http.asInstanceOf[Http[R1, E1, Any, Nothing]]
    }

  /**
   * Logical operator to decide which middleware to select based on the
   * predicate.
   */
  def ifThenElse[A]: PartialIfThenElse[A] = new PartialIfThenElse(())

  /**
   * Logical operator to decide which middleware to select based on the
   * predicate effect.
   */
  def ifThenElseZIO[A]: PartialIfThenElseZIO[A] = new PartialIfThenElseZIO(())

  /**
   * Creates a new middleware using transformation functions
   */
  def intercept[A, B]: PartialIntercept[A, B] = new PartialIntercept[A, B](())

  /**
   * Creates a new middleware using effectful transformation functions
   */
  def interceptZIO[A, B]: PartialInterceptZIO[A, B] = new PartialInterceptZIO[A, B](())

  /**
   * Creates a middleware which always succeed with specified value
   */
  def succeed[B](b: B): Middleware[Any, Nothing, Nothing, Any, Any, B] = fromHttp(Http.succeed(b))

  final class PartialCollect[AOut](val unit: Unit) extends AnyVal {
    def apply[R, E, AIn, BIn, BOut](
      f: PartialFunction[AOut, Middleware[R, E, AIn, BIn, AOut, BOut]],
    ): Middleware[R, E, AIn, BIn, AOut, BOut] =
      Middleware.fromHttp(Http.collect[AOut] { case aout if f.isDefinedAt(aout) => f(aout) }).flatten
  }

  final class PartialCollectZIO[AOut](val unit: Unit) extends AnyVal {
    def apply[R, E, AIn, BIn, BOut](
      f: PartialFunction[AOut, ZIO[R, E, Middleware[R, E, AIn, BIn, AOut, BOut]]],
    ): Middleware[R, E, AIn, BIn, AOut, BOut] =
      Middleware.fromHttp(Http.collectZIO[AOut] { case aout if f.isDefinedAt(aout) => f(aout) }).flatten
  }

  final class PartialIntercept[A, B](val unit: Unit) extends AnyVal {
    def apply[S, BOut](incoming: A => S)(outgoing: (B, S) => BOut): Middleware[Any, Nothing, A, B, A, BOut] =
      interceptZIO[A, B](a => ZIO.succeed(incoming(a)))((b, s) => ZIO.succeed(outgoing(b, s)))
  }

  final class PartialInterceptZIO[A, B](val unit: Unit) extends AnyVal {
    def apply[R, E, S, BOut](
      incoming: A => ZIO[R, Option[E], S],
    ): PartialInterceptOutgoingZIO[R, E, A, S, B] =
      new PartialInterceptOutgoingZIO(incoming)
  }

  final class PartialInterceptOutgoingZIO[-R, +E, A, +S, B](val incoming: A => ZIO[R, Option[E], S]) extends AnyVal {
    def apply[R1 <: R, E1 >: E, BOut](
      outgoing: (B, S) => ZIO[R1, Option[E1], BOut],
    ): Middleware[R1, E1, A, B, A, BOut] =
      new Middleware[R1, E1, A, B, A, BOut] {
        override def apply[R2 <: R1, E2 >: E1](http: Http[R2, E2, A, B]): Http[R2, E2, A, BOut] =
          Http.fromOptionFunction[A] { a =>
            for {
              s <- incoming(a)
              b <- http(a)
              c <- outgoing(b, s)
            } yield c
          }
      }
  }

  final class PartialCodec[AOut, BIn](val unit: Unit) extends AnyVal {
    def apply[E, AIn, BOut](
      decoder: AOut => Either[E, AIn],
      encoder: BIn => Either[E, BOut],
    ): Middleware[Any, E, AIn, BIn, AOut, BOut] =
      Middleware.identity.mapZIO((b: BIn) => ZIO.fromEither(encoder(b))).contramapZIO(a => ZIO.fromEither(decoder(a)))
  }

  final class PartialIfThenElse[AOut](val unit: Unit) extends AnyVal {
    def apply[R, E, AIn, BIn, BOut](cond: AOut => Boolean)(
      isTrue: AOut => Middleware[R, E, AIn, BIn, AOut, BOut],
      isFalse: AOut => Middleware[R, E, AIn, BIn, AOut, BOut],
    ): Middleware[R, E, AIn, BIn, AOut, BOut] =
      Middleware
        .fromHttp(Http.fromFunction[AOut] { a => if (cond(a)) isTrue(a) else isFalse(a) })
        .flatten
  }

  final class PartialIfThenElseZIO[AOut](val unit: Unit) extends AnyVal {
    def apply[R, E, AIn, BIn, BOut](cond: AOut => ZIO[R, E, Boolean])(
      isTrue: AOut => Middleware[R, E, AIn, BIn, AOut, BOut],
      isFalse: AOut => Middleware[R, E, AIn, BIn, AOut, BOut],
    ): Middleware[R, E, AIn, BIn, AOut, BOut] =
      Middleware
        .fromHttp(Http.fromFunctionZIO[AOut] { a => cond(a).map(b => if (b) isTrue(a) else isFalse(a)) })
        .flatten
  }

  final class PartialCodecZIO[AOut, BIn](val unit: Unit) extends AnyVal {
    def apply[R, E, AIn, BOut](
      decoder: AOut => ZIO[R, E, AIn],
      encoder: BIn => ZIO[R, E, BOut],
    ): Middleware[R, E, AIn, BIn, AOut, BOut] =
      Middleware.identity.mapZIO(encoder).contramapZIO(decoder)
  }

  final class PartialCodecHttp[AOut, BIn](val unit: Unit) extends AnyVal {
    def apply[R, E, AIn, BOut](
      decoder: Http[R, E, AOut, AIn],
      encoder: Http[R, E, BIn, BOut],
    ): Middleware[R, E, AIn, BIn, AOut, BOut] =
      new Middleware[R, E, AIn, BIn, AOut, BOut] {
        override def apply[R1 <: R, E1 >: E](http: Http[R1, E1, AIn, BIn]): Http[R1, E1, AOut, BOut] =
          decoder >>> http >>> encoder
      }
  }

  final class PartialContraMapZIO[-R, +E, +AIn, -BIn, -AOut, +BOut, AOut0](
    val self: Middleware[R, E, AIn, BIn, AOut, BOut],
  ) extends AnyVal {
    def apply[R1 <: R, E1 >: E](f: AOut0 => ZIO[R1, E1, AOut]): Middleware[R1, E1, AIn, BIn, AOut0, BOut] =
      new Middleware[R1, E1, AIn, BIn, AOut0, BOut] {
        override def apply[R2 <: R1, E2 >: E1](http: Http[R2, E2, AIn, BIn]): Http[R2, E2, AOut0, BOut] =
          self(http).contramapZIO(a => f(a))
      }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy