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

caliban.wrappers.Wrapper.scala Maven / Gradle / Ivy

The newest version!
package caliban.wrappers

import caliban.CalibanError.{ ExecutionError, ParsingError, ValidationError }
import caliban._
import caliban.execution.{ ExecutionRequest, FieldInfo }
import caliban.introspection.adt.__Introspection
import caliban.parsing.adt.Document
import caliban.wrappers.Wrapper.CombinedWrapper
import zio.query.ZQuery
import zio.{ Exit, Trace, URIO, ZIO }
import zio.stacktracer.TracingImplicits.disableAutoTrace

import scala.annotation.tailrec
import scala.collection.mutable.ListBuffer

/**
 * A `Wrapper[-R]` represents an extra layer of computation that can be applied on top of Caliban's query handling.
 * There are different base types of wrappers:
 * - `OverallWrapper` to wrap the whole query processing
 * - `ParsingWrapper` to wrap the query parsing only
 * - `ValidationWrapper` to wrap the query validation only
 * - `ExecutionWrapper` to wrap the query execution only
 * - `FieldWrapper` to wrap each field execution
 *
 * It is also possible to combine wrappers using `|+|` and to build a wrapper effectfully with `EffectfulWrapper`.
 *
 * Implementations can control the order at which this wrapper is executed by overriding the `priority` value.
 * Setting a higher `priority` value will be executed first.
 */
sealed trait Wrapper[-R] extends GraphQLAspect[Nothing, R] { self =>
  val priority: Int = 0

  def |+|[R1 <: R](that: Wrapper[R1]): Wrapper[R1] = that match {
    case Wrapper.Empty => self
    case _             => CombinedWrapper(List(self, that))
  }

  def apply[R1 <: R](that: GraphQL[R1]): GraphQL[R1] =
    that.withWrapper(self)

  // Disables tracing only for wrappers in the caliban package
  final private[caliban] implicit def trace: Trace = Trace.empty
}

object Wrapper {

  /**
   * A wrapper that doesn't do anything.
   * Useful for cases where we want to programmatically decide whether we'll use a wrapper or not
   */
  case object Empty extends Wrapper[Any] {
    override def |+|[R1 <: Any](that: Wrapper[R1]): Wrapper[R1]   = that
    override def apply[R1 <: Any](that: GraphQL[R1]): GraphQL[R1] = that
  }

  def empty[R]: Wrapper[R] = Empty

  /**
   * Suspends the creation of a wrapper which allows capturing any side effects in the creation of a wrapper.
   * The wrapper will be recreated for each query.
   * @param wrapper the wrapper to suspend
   */
  def suspend[R](wrapper: => Wrapper[R]): Wrapper[R] = SuspendedWrapper[R](() => wrapper)

  sealed trait SimpleWrapper[-R, E, A, Info] extends Wrapper[R] {
    def wrap[R1 <: R](f: Info => ZIO[R1, E, A]): Info => ZIO[R1, E, A]
  }

  /**
   * Wrapper for the whole query processing.
   * Wraps a function from a request `GraphQLRequest` to a `URIO[R, GraphQLResponse[CalibanError]]`.
   */
  trait OverallWrapper[-R] extends SimpleWrapper[R, Nothing, GraphQLResponse[CalibanError], GraphQLRequest]

  /**
   * Wrapper for the query parsing stage.
   * Wraps a function from a query `String` to a `ZIO[R, ParsingError, Document]`.
   */
  trait ParsingWrapper[-R] extends SimpleWrapper[R, ParsingError, Document, String]

  /**
   * Wrapper for the query validation stage.
   * Wraps a function from a `Document` to a `ZIO[R, ValidationError, ExecutionRequest]`.
   */
  trait ValidationWrapper[-R] extends SimpleWrapper[R, ValidationError, ExecutionRequest, Document] { self =>

    /**
     * Returns a new wrapper which skips the [[wrap]] function if the query is an introspection query.
     */
    final def skipForIntrospection: ValidationWrapper[R] = new ValidationWrapper[R] {
      override val priority: Int = self.priority

      override def wrap[R1 <: R](
        f: Document => ZIO[R1, ValidationError, ExecutionRequest]
      ): Document => ZIO[R1, ValidationError, ExecutionRequest] =
        (doc: Document) =>
          if (doc.isIntrospection) f(doc)
          else self.wrap(f)(doc)
    }
  }

  /**
   * Wrapper for the query execution stage.
   * Wraps a function from an `ExecutionRequest` to a `URIO[R, GraphQLResponse[CalibanError]]`.
   */
  trait ExecutionWrapper[-R] extends SimpleWrapper[R, Nothing, GraphQLResponse[CalibanError], ExecutionRequest]

  /**
   * Wrapper for each individual field.
   * Takes a function from a `ZQuery[R, Nothing, ResponseValue]` and a `FieldInfo` and that returns a
   * `ZQuery[R, CalibanError, ResponseValue]`.
   * If `wrapPureValues` is true, every single field will be wrapped, which could have an impact on performances.
   * If false, simple pure values will be ignored.
   */
  abstract class FieldWrapper[-R](val wrapPureValues: Boolean = false) extends Wrapper[R] {
    def wrap[R1 <: R](
      query: ZQuery[R1, ExecutionError, ResponseValue],
      info: FieldInfo
    ): ZQuery[R1, ExecutionError, ResponseValue]
  }

  /**
   * Wrapper for the introspection query processing.
   * Takes a function from a `ZIO[R, ExecutionError, __Introspection]` and that returns a
   * `ZIO[R, ExecutionError, __Introspection]`.
   */
  trait IntrospectionWrapper[-R] extends Wrapper[R] {
    def wrap[R1 <: R](effect: ZIO[R1, ExecutionError, __Introspection]): ZIO[R1, ExecutionError, __Introspection]
  }

  /**
   * Wrapper that combines multiple wrappers.
   * @param wrappers a list of wrappers
   */
  case class CombinedWrapper[-R](wrappers: List[Wrapper[R]]) extends Wrapper[R] {
    override def |+|[R1 <: R](that: Wrapper[R1]): Wrapper[R1] = that match {
      case CombinedWrapper(other) => copy(wrappers = wrappers ++ other)
      case other                  => copy(wrappers = wrappers :+ other)
    }

  }

  /**
   * A wrapper that requires an effect to be built. The effect will be run for each query.
   * @param wrapper an effect that builds a wrapper
   */
  case class EffectfulWrapper[-R](wrapper: URIO[R, Wrapper[R]]) extends Wrapper[R]

  private case class SuspendedWrapper[-R](wrapper: () => Wrapper[R]) extends Wrapper[R]

  private[caliban] def wrap[R1 >: R, R, E, A, Info](
    process: Info => ZIO[R1, E, A]
  )(wrappers: List[SimpleWrapper[R, E, A, Info]], info: Info): ZIO[R, E, A] = {
    @tailrec
    def loop(process: Info => ZIO[R, E, A], wrappers: List[SimpleWrapper[R, E, A, Info]]): Info => ZIO[R, E, A] =
      wrappers match {
        case Nil             => process
        case wrapper :: tail => loop(wrapper.wrap((info: Info) => process(info)), tail)
      }
    loop(process, wrappers)(info)
  }

  private val emptyWrappers =
    Exit.succeed((Nil, Nil, Nil, Nil, Nil, Nil))

  private[caliban] def decompose[R](wrappers: List[Wrapper[R]])(implicit trace: Trace): URIO[
    R,
    (
      List[OverallWrapper[R]],
      List[ParsingWrapper[R]],
      List[ValidationWrapper[R]],
      List[ExecutionWrapper[R]],
      List[FieldWrapper[R]],
      List[IntrospectionWrapper[R]]
    )
  ] =
    if (wrappers.isEmpty) emptyWrappers
    else
      ZIO.suspendSucceed {
        val o = ListBuffer.empty[OverallWrapper[R]]
        val p = ListBuffer.empty[ParsingWrapper[R]]
        val v = ListBuffer.empty[ValidationWrapper[R]]
        val e = ListBuffer.empty[ExecutionWrapper[R]]
        val f = ListBuffer.empty[FieldWrapper[R]]
        val i = ListBuffer.empty[IntrospectionWrapper[R]]

        def loop(wrapper: Wrapper[R]): Option[URIO[R, Unit]] = wrapper match {
          case wrapper: OverallWrapper[R]       => o append wrapper; None
          case wrapper: ParsingWrapper[R]       => p append wrapper; None
          case wrapper: ValidationWrapper[R]    => v append wrapper; None
          case wrapper: ExecutionWrapper[R]     => e append wrapper; None
          case wrapper: FieldWrapper[R]         => f append wrapper; None
          case wrapper: IntrospectionWrapper[R] => i append wrapper; None
          case SuspendedWrapper(f)              => loop(f())
          case CombinedWrapper(wrappers)        =>
            wrappers.flatMap(loop) match {
              case Nil => None
              case fs  => Some(ZIO.collectAllDiscard(fs))
            }
          case EffectfulWrapper(wrapper)        =>
            Some(wrapper.flatMap {
              loop(_) match {
                case None    => Exit.unit
                case Some(w) => w
              }
            })
          case Wrapper.Empty                    => None
        }

        def finalize[W <: Wrapper[R]](buffer: ListBuffer[W]): List[W] = buffer.sortBy(_.priority).result()

        def result() =
          (finalize(o), finalize(p), finalize(v), finalize(e), finalize(f), finalize(i))

        wrappers.flatMap(loop) match {
          case Nil => Exit.succeed(result())
          case fs  => ZIO.collectAllDiscard(fs).as(result())
        }
      }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy