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

sttp.tapir.Endpoint.scala Maven / Gradle / Ivy

There is a newer version: 1.11.9
Show newest version
package sttp.tapir

import sttp.capabilities.WebSockets
import sttp.model.Method
import sttp.monad.IdentityMonad
import sttp.monad.syntax._
import sttp.shared.Identity
import sttp.tapir.EndpointInput.{FixedMethod, PathCapture, Query}
import sttp.tapir.EndpointOutput.OneOfVariant
import sttp.tapir.internal._
import sttp.tapir.macros.{EndpointErrorOutputsMacros, EndpointInputsMacros, EndpointOutputsMacros, EndpointSecurityInputsMacros}
import sttp.tapir.server.{
  PartialServerEndpoint,
  PartialServerEndpointSync,
  PartialServerEndpointWithSecurityOutput,
  PartialServerEndpointWithSecurityOutputSync,
  ServerEndpoint
}
import sttp.tapir.typelevel.{ErasureSameAsType, ParamConcat}

import scala.reflect.ClassTag

/** A description of an endpoint with the given inputs & outputs. The inputs are divided into two parts: security (`A`) and regular inputs
  * (`I`). There are also two kinds of outputs: error outputs (`E`) and regular outputs (`O`).
  *
  * In case there are no security inputs, the [[PublicEndpoint]] alias can be used, which omits the `A` parameter type.
  *
  * An endpoint can be interpreted as a server, client or documentation. The endpoint requires that server/client interpreters meet the
  * capabilities specified by `R` (if any).
  *
  * When interpreting an endpoint as a server, the inputs are decoded and the security logic is run first, before decoding the body in the
  * regular inputs. This allows short-circuiting further processing in case security checks fail. Server logic can be provided using
  * [[EndpointServerLogicOps.serverSecurityLogic]] variants for secure endpoints, and [[EndpointServerLogicOps.serverLogic]] variants for
  * public endpoints; when using a synchronous server, you can also use the more concise [[EndpointServerLogicOps.handle]] methods, which
  * work the save as above, but have the "effect" type fixed to [[Identity]].
  *
  * A concise description of an endpoint can be generated using the [[EndpointMetaOps.show]] method.
  *
  * @tparam SECURITY_INPUT
  *   Security input parameter types, abbreviated as `A`.
  * @tparam INPUT
  *   Input parameter types, abbreviated as `I`.
  * @tparam ERROR_OUTPUT
  *   Error output parameter types, abbreviated as `E`.
  * @tparam OUTPUT
  *   Output parameter types, abbreviated as `O`.
  * @tparam R
  *   The capabilities that are required by this endpoint's inputs/outputs. This might be `Any` (no requirements),
  *   [[sttp.capabilities.Effect]] (the interpreter must support the given effect type), [[sttp.capabilities.Streams]] (the ability to send
  *   and receive streaming bodies) or [[sttp.capabilities.WebSockets]] (the ability to handle websocket requests).
  */
case class Endpoint[SECURITY_INPUT, INPUT, ERROR_OUTPUT, OUTPUT, -R](
    securityInput: EndpointInput[SECURITY_INPUT],
    input: EndpointInput[INPUT],
    errorOutput: EndpointOutput[ERROR_OUTPUT],
    output: EndpointOutput[OUTPUT],
    info: EndpointInfo
) extends EndpointSecurityInputsOps[SECURITY_INPUT, INPUT, ERROR_OUTPUT, OUTPUT, R]
    with EndpointInputsOps[SECURITY_INPUT, INPUT, ERROR_OUTPUT, OUTPUT, R]
    with EndpointErrorOutputsOps[SECURITY_INPUT, INPUT, ERROR_OUTPUT, OUTPUT, R]
    with EndpointErrorOutputVariantsOps[SECURITY_INPUT, INPUT, ERROR_OUTPUT, OUTPUT, R]
    with EndpointOutputsOps[SECURITY_INPUT, INPUT, ERROR_OUTPUT, OUTPUT, R]
    with EndpointInfoOps[R]
    with EndpointMetaOps
    with EndpointServerLogicOps[SECURITY_INPUT, INPUT, ERROR_OUTPUT, OUTPUT, R] { outer =>

  override type EndpointType[_A, _I, _E, _O, -_R] = Endpoint[_A, _I, _E, _O, _R]
  override type ThisType[-_R] = Endpoint[SECURITY_INPUT, INPUT, ERROR_OUTPUT, OUTPUT, _R]
  override private[tapir] def withSecurityInput[A2, R2](
      securityInput: EndpointInput[A2]
  ): Endpoint[A2, INPUT, ERROR_OUTPUT, OUTPUT, R with R2] =
    this.copy(securityInput = securityInput)
  override private[tapir] def withInput[I2, R2](input: EndpointInput[I2]): Endpoint[SECURITY_INPUT, I2, ERROR_OUTPUT, OUTPUT, R with R2] =
    this.copy(input = input)
  override private[tapir] def withErrorOutput[E2, R2](
      errorOutput: EndpointOutput[E2]
  ): Endpoint[SECURITY_INPUT, INPUT, E2, OUTPUT, R with R2] =
    this.copy(errorOutput = errorOutput)
  override private[tapir] def withErrorOutputVariant[E2, R2](
      errorOutput: EndpointOutput[E2],
      embedE: ERROR_OUTPUT => E2
  ): Endpoint[SECURITY_INPUT, INPUT, E2, OUTPUT, R with R2] =
    this.copy(errorOutput = errorOutput)
  override private[tapir] def withOutput[O2, R2](output: EndpointOutput[O2]): Endpoint[SECURITY_INPUT, INPUT, ERROR_OUTPUT, O2, R with R2] =
    this.copy(output = output)
  override private[tapir] def withInfo(info: EndpointInfo): Endpoint[SECURITY_INPUT, INPUT, ERROR_OUTPUT, OUTPUT, R] =
    this.copy(info = info)
  override protected def showType: String = "Endpoint"
}

trait EndpointSecurityInputsOps[A, I, E, O, -R] extends EndpointSecurityInputsMacros[A, I, E, O, R] {
  type EndpointType[_A, _I, _E, _O, -_R]
  def securityInput: EndpointInput[A]
  private[tapir] def withSecurityInput[A2, R2](securityInput: EndpointInput[A2]): EndpointType[A2, I, E, O, R with R2]

  def securityIn[B, AB](i: EndpointInput[B])(implicit concat: ParamConcat.Aux[A, B, AB]): EndpointType[AB, I, E, O, R] =
    withSecurityInput(securityInput.and(i))

  def prependSecurityIn[B, BA](i: EndpointInput[B])(implicit concat: ParamConcat.Aux[B, A, BA]): EndpointType[BA, I, E, O, R] =
    withSecurityInput(i.and(securityInput))

  def mapSecurityIn[AA](m: Mapping[A, AA]): EndpointType[AA, I, E, O, R] =
    withSecurityInput(securityInput.map(m))

  def mapSecurityIn[AA](f: A => AA)(g: AA => A): EndpointType[AA, I, E, O, R] =
    withSecurityInput(securityInput.map(f)(g))

  def mapSecurityInDecode[AA](f: A => DecodeResult[AA])(g: AA => A): EndpointType[AA, I, E, O, R] =
    withSecurityInput(securityInput.mapDecode(f)(g))
}

trait EndpointInputsOps[A, I, E, O, -R] extends EndpointInputsMacros[A, I, E, O, R] {
  type EndpointType[_A, _I, _E, _O, -_R]
  def input: EndpointInput[I]
  private[tapir] def withInput[I2, R2](input: EndpointInput[I2]): EndpointType[A, I2, E, O, R with R2]

  def get: EndpointType[A, I, E, O, R] = method(Method.GET)
  def post: EndpointType[A, I, E, O, R] = method(Method.POST)
  def head: EndpointType[A, I, E, O, R] = method(Method.HEAD)
  def put: EndpointType[A, I, E, O, R] = method(Method.PUT)
  def delete: EndpointType[A, I, E, O, R] = method(Method.DELETE)
  def options: EndpointType[A, I, E, O, R] = method(Method.OPTIONS)
  def patch: EndpointType[A, I, E, O, R] = method(Method.PATCH)
  def connect: EndpointType[A, I, E, O, R] = method(Method.CONNECT)
  def trace: EndpointType[A, I, E, O, R] = method(Method.TRACE)
  def method(m: sttp.model.Method): EndpointType[A, I, E, O, R] = in(FixedMethod(m, Codec.idPlain(), EndpointIO.Info.empty))

  def in[J, IJ](i: EndpointInput[J])(implicit concat: ParamConcat.Aux[I, J, IJ]): EndpointType[A, IJ, E, O, R] =
    withInput(input.and(i))

  def prependIn[J, JI](i: EndpointInput[J])(implicit concat: ParamConcat.Aux[J, I, JI]): EndpointType[A, JI, E, O, R] =
    withInput(i.and(input))

  def in[BS, J, IJ, R2](i: StreamBodyIO[BS, J, R2])(implicit concat: ParamConcat.Aux[I, J, IJ]): EndpointType[A, IJ, E, O, R with R2] =
    withInput(input.and(i.toEndpointIO))

  def prependIn[BS, J, JI, R2](i: StreamBodyIO[BS, J, R2])(implicit
      concat: ParamConcat.Aux[J, I, JI]
  ): EndpointType[A, JI, E, O, R with R2] =
    withInput(i.toEndpointIO.and(input))

  def mapIn[II](m: Mapping[I, II]): EndpointType[A, II, E, O, R] =
    withInput(input.map(m))

  def mapIn[II](f: I => II)(g: II => I): EndpointType[A, II, E, O, R] =
    withInput(input.map(f)(g))

  def mapInDecode[II](f: I => DecodeResult[II])(g: II => I): EndpointType[A, II, E, O, R] =
    withInput(input.mapDecode(f)(g))
}

trait EndpointErrorOutputsOps[A, I, E, O, -R] extends EndpointErrorOutputsMacros[A, I, E, O, R] {
  type EndpointType[_A, _I, _E, _O, -_R]
  def errorOutput: EndpointOutput[E]
  private[tapir] def withErrorOutput[E2, R2](output: EndpointOutput[E2]): EndpointType[A, I, E2, O, R with R2]

  def errorOut[F, EF](o: EndpointOutput[F])(implicit ts: ParamConcat.Aux[E, F, EF]): EndpointType[A, I, EF, O, R] =
    withErrorOutput(errorOutput.and(o))

  def prependErrorOut[F, FE](o: EndpointOutput[F])(implicit ts: ParamConcat.Aux[F, E, FE]): EndpointType[A, I, FE, O, R] =
    withErrorOutput(o.and(errorOutput))

  def mapErrorOut[EE](m: Mapping[E, EE]): EndpointType[A, I, EE, O, R] =
    withErrorOutput(errorOutput.map(m))

  def mapErrorOutDecode[EE](f: E => DecodeResult[EE])(g: EE => E): EndpointType[A, I, EE, O, R] =
    withErrorOutput(errorOutput.mapDecode(f)(g))

  // mapError(f)(g) is defined in EndpointErrorOutputVariantsOps
}

trait EndpointErrorOutputVariantsOps[A, I, E, O, -R] {
  type EndpointType[_A, _I, _E, _O, -_R]
  def errorOutput: EndpointOutput[E]
  private[tapir] def withErrorOutputVariant[E2, R2](output: EndpointOutput[E2], embedE: E => E2): EndpointType[A, I, E2, O, R with R2]

  /** Replaces the current error output with a [[Tapir.oneOf]] output, where:
    *   - the first output variant is the current output: `oneOfVariant(errorOutput)`
    *   - the second output variant is the given `o`
    *
    * The variant for the current endpoint output will be created using [[Tapir.oneOfVariant]]. Hence, the current output will be used if
    * the run-time class of the output matches `E`. If the erasure of the `E` type is different from `E`, there will be a compile-time
    * failure, as no such run-time check is possible. In this case, use [[errorOutVariantsFromCurrent]] and create a variant using one of
    * the other variant factory methods (e.g. [[Tapir.oneOfVariantValueMatcher]]).
    *
    * During encoding/decoding, the new `o` variant will be considered after the current variant.
    *
    * Usage example:
    *
    * {{{
    *   sealed trait Parent
    *   case class Child1(v: String) extends Parent
    *   case class Child2(v: String) extends Parent
    *
    *   val e: PublicEndpoint[Unit, Parent, Unit, Any] = endpoint
    *     .errorOut(stringBody.mapTo[Child1])
    *     .errorOutVariant[Parent](oneOfVariant(stringBody.mapTo[Child2]))
    * }}}
    *
    * Adding error output variants is useful when extending the error outputs in a [[PartialServerEndpoint]], created using
    * [[EndpointServerLogicOps.serverSecurityLogic]].
    *
    * @param o
    *   The variant to add. Can be created given an output with one of the [[Tapir.oneOfVariant]] methods.
    * @tparam E2
    *   A common supertype of the new variant and the current output `E`.
    */
  def errorOutVariant[E2 >: E](
      o: OneOfVariant[_ <: E2]
  )(implicit ct: ClassTag[E], eEqualToErasure: ErasureSameAsType[E]): EndpointType[A, I, E2, O, R] =
    withErrorOutputVariant(oneOf[E2](oneOfVariant[E](errorOutput), o), identity)

  /** Replaces the current error output with a [[Tapir.oneOf]] output, where:
    *   - the first output variant is the given `o`
    *   - the second, default output variant is the current output: `oneOfDefaultVariant(errorOutput)`
    *
    * Useful for adding specific error variants, while the more general ones are already covered by the existing error output.
    *
    * During encoding/decoding, the new `o` variant will be considered before the current variant.
    *
    * Adding error output variants is useful when extending the error outputs in a [[PartialServerEndpoint]], created using
    * [[EndpointServerLogicOps.serverSecurityLogic]].
    *
    * @param o
    *   The variant to add. Can be created given an output with one of the [[Tapir.oneOfVariant]] methods.
    * @tparam E2
    *   A common supertype of the new variant and the current output `E`.
    */
  def errorOutVariantPrepend[E2 >: E](o: OneOfVariant[_ <: E2]): EndpointType[A, I, E2, O, R] =
    withErrorOutputVariant(oneOf[E2](o, oneOfDefaultVariant(errorOutput)), identity)

  /** Same as [[errorOutVariantPrepend]], but allows appending multiple variants in one go. */
  def errorOutVariantsPrepend[E2 >: E](first: OneOfVariant[_ <: E2], other: OneOfVariant[_ <: E2]*): EndpointType[A, I, E2, O, R] =
    withErrorOutputVariant(oneOf[E2](first, other :+ oneOfDefaultVariant(errorOutput): _*), identity)

  /** Same as [[errorOutVariant]], but allows appending multiple variants in one go. */
  def errorOutVariants[E2 >: E](first: OneOfVariant[_ <: E2], other: OneOfVariant[_ <: E2]*)(implicit
      ct: ClassTag[E],
      eEqualToErasure: ErasureSameAsType[E]
  ): EndpointType[A, I, E2, O, R] =
    withErrorOutputVariant(oneOf[E2](oneOfVariant[E](errorOutput), first +: other: _*), identity)

  /** Replace the error output with a [[Tapir.oneOf]] output, using the variants returned by `variants`. The current output should be
    * included in one of the returned variants.
    *
    * Allows creating the variant list in a custom order, placing the current variant in an arbitrary position, and using default variants
    * if necessary.
    *
    * Adding error output variants is useful when extending the error outputs in a [[PartialServerEndpoint]], created using
    * [[EndpointServerLogicOps.serverSecurityLogic]].
    *
    * @tparam E2
    *   A common supertype of the new variant and the current output `E`.
    */
  def errorOutVariantsFromCurrent[E2 >: E](variants: EndpointOutput[E] => List[OneOfVariant[_ <: E2]]): EndpointType[A, I, E2, O, R] =
    withErrorOutputVariant(EndpointOutput.OneOf[E2, E2](variants(errorOutput), Mapping.id), identity)

  /** Adds a new error variant, where the current error output is represented as a `Left`, and the given one as a `Right`. */
  def errorOutEither[E2](o: EndpointOutput[E2]): EndpointType[A, I, Either[E, E2], O, R] =
    withErrorOutputVariant(
      oneOf(
        oneOfVariantValueMatcher(o.map(Right(_))(_.value)) { case Right(_) =>
          true
        },
        oneOfVariantValueMatcher(errorOutput.map(Left(_))(_.value)) { case Left(_) =>
          true
        }
      ),
      Left(_)
    )

  def mapErrorOut[EE](f: E => EE)(g: EE => E): EndpointType[A, I, EE, O, R] =
    withErrorOutputVariant(errorOutput.map(f)(g), f)
}

trait EndpointOutputsOps[A, I, E, O, -R] extends EndpointOutputsMacros[A, I, E, O, R] {
  type EndpointType[_A, _I, _E, _O, -_R]
  def output: EndpointOutput[O]
  private[tapir] def withOutput[O2, R2](input: EndpointOutput[O2]): EndpointType[A, I, E, O2, R with R2]

  def out[P, OP](i: EndpointOutput[P])(implicit ts: ParamConcat.Aux[O, P, OP]): EndpointType[A, I, E, OP, R] =
    withOutput(output.and(i))

  def prependOut[P, PO](i: EndpointOutput[P])(implicit ts: ParamConcat.Aux[P, O, PO]): EndpointType[A, I, E, PO, R] =
    withOutput(i.and(output))

  def out[BS, P, OP, R2](i: StreamBodyIO[BS, P, R2])(implicit ts: ParamConcat.Aux[O, P, OP]): EndpointType[A, I, E, OP, R with R2] =
    withOutput(output.and(i.toEndpointIO))

  def prependOut[BS, P, PO, R2](i: StreamBodyIO[BS, P, R2])(implicit ts: ParamConcat.Aux[P, O, PO]): EndpointType[A, I, E, PO, R with R2] =
    withOutput(i.toEndpointIO.and(output))

  def out[PIPE_REQ_RESP, P, OP, R2](i: WebSocketBodyOutput[PIPE_REQ_RESP, _, _, P, R2])(implicit
      ts: ParamConcat.Aux[O, P, OP]
  ): EndpointType[A, I, E, OP, R with R2 with WebSockets] = withOutput(output.and(i.toEndpointOutput))

  def prependOut[PIPE_REQ_RESP, P, PO, R2](i: WebSocketBodyOutput[PIPE_REQ_RESP, _, _, P, R2])(implicit
      ts: ParamConcat.Aux[P, O, PO]
  ): EndpointType[A, I, E, PO, R with R2 with WebSockets] = withOutput(i.toEndpointOutput.and(output))

  def mapOut[OO](m: Mapping[O, OO]): EndpointType[A, I, E, OO, R] =
    withOutput(output.map(m))

  def mapOut[OO](f: O => OO)(g: OO => O): EndpointType[A, I, E, OO, R] =
    withOutput(output.map(f)(g))

  def mapOutDecode[OO](f: O => DecodeResult[OO])(g: OO => O): EndpointType[A, I, E, OO, R] =
    withOutput(output.mapDecode(f)(g))
}

trait EndpointInfoOps[-R] {
  type ThisType[-_R]
  def info: EndpointInfo
  private[tapir] def withInfo(info: EndpointInfo): ThisType[R]

  def name(n: String): ThisType[R] = withInfo(info.name(n))
  def summary(s: String): ThisType[R] = withInfo(info.summary(s))
  def description(d: String): ThisType[R] = withInfo(info.description(d))
  def deprecated(): ThisType[R] = withInfo(info.deprecated(true))
  def attribute[T](k: AttributeKey[T]): Option[T] = info.attribute(k)
  def attribute[T](k: AttributeKey[T], v: T): ThisType[R] = withInfo(info.attribute(k, v))

  /** Append `ts` to the existing tags. */
  def tags(ts: List[String]): ThisType[R] = withInfo(info.tags(ts))

  /** Append `t` to the existing tags. */
  def tag(t: String): ThisType[R] = withInfo(info.tag(t))

  /** Overwrite the existing tags with `ts`. */
  def withTags(ts: List[String]): ThisType[R] = withInfo(info.withTags(ts))

  /** Overwrite the existing tags with a single tag `t`. */
  def withTag(t: String): ThisType[R] = withInfo(info.withTag(t))

  /** Remove all tags from this endpoint. */
  def withoutTags: ThisType[R] = withInfo(info.withoutTags)

  def info(i: EndpointInfo): ThisType[R] = withInfo(i)
}

trait EndpointMetaOps {
  def securityInput: EndpointInput[_]
  def input: EndpointInput[_]
  def errorOutput: EndpointOutput[_]
  def output: EndpointOutput[_]
  def info: EndpointInfo

  /** Shortened information about the endpoint. If the endpoint is named, returns the name, e.g. `[my endpoint]`. Otherwise, returns the
    * string representation of the method (if any) and path, e.g. `POST /books/add`
    */
  lazy val showShort: String = info.name match {
    case None       => s"${method.map(_.toString()).getOrElse("*")} ${showPathTemplate(showQueryParam = None)}"
    case Some(name) => s"[$name]"
  }

  /** Basic information about the endpoint, excluding mapping information, with inputs sorted (first the method, then path, etc.). E.g.:
    * `POST /books /add {header Authorization} {body as application/json (UTF-8)} -> {body as text/plain (UTF-8)}/-`
    */
  lazy val show: String = {
    def showOutputs(o: EndpointOutput[_]): String = showOneOf(o.asBasicOutputsList.map(os => showMultiple(os.sortByType)))

    val namePrefix = info.name.map("[" + _ + "] ").getOrElse("")
    val showInputs = showMultiple(
      (securityInput.asVectorOfBasicInputs() ++ input.asVectorOfBasicInputs()).sortBy(basicInputSortIndex)
    )
    val showSuccessOutputs = showOutputs(output)
    val showErrorOutputs = showOutputs(errorOutput)

    s"$namePrefix$showInputs -> $showErrorOutputs/$showSuccessOutputs"
  }

  /** Detailed description of the endpoint, with inputs/outputs represented in the same order as originally defined, including mapping
    * information. E.g.:
    *
    * {{{
    * Endpoint(securityin: -, in: /books POST /add {body as application/json (UTF-8)} {header Authorization}, errout: {body as text/plain (UTF-8)}, out: -)
    * }}}
    */
  lazy val showDetail: String =
    s"$showType${info.name.map("[" + _ + "]").getOrElse("")}(securityin: ${securityInput.show}, in: ${input.show}, errout: ${errorOutput.show}, out: ${output.show})"
  protected def showType: String

  /** Equivalent to `.toString`, shows the whole case class structure. */
  def showRaw: String = toString

  /** Shows endpoint path, by default all parametrised path and query components are replaced by {param_name} or {paramN}, e.g. for
    * {{{
    * endpoint.in("p1" / path[String] / query[String]("par2"))
    * }}}
    * returns `/p1/{param1}?par2={par2}`
    *
    * @param includeAuth
    *   Should authentication inputs be included in the result.
    * @param showNoPathAs
    *   How to show the path if the endpoint does not define any path inputs.
    * @param showPathsAs
    *   How to show [[Tapir.paths]] inputs (if at all), which capture multiple paths segments
    * @param showQueryParamsAs
    *   How to show [[Tapir.queryParams]] inputs (if at all), which capture multiple query parameters
    */
  def showPathTemplate(
      showPathParam: (Int, PathCapture[_]) => String = (index, pc) => pc.name.map(name => s"{$name}").getOrElse(s"{param$index}"),
      showQueryParam: Option[(Int, Query[_]) => String] = Some((_, q) => s"${q.name}={${q.name}}"),
      includeAuth: Boolean = true,
      showNoPathAs: String = "*",
      showPathsAs: Option[String] = Some("*"),
      showQueryParamsAs: Option[String] = Some("*")
  ): String = ShowPathTemplate(this)(showPathParam, showQueryParam, includeAuth, showNoPathAs, showPathsAs, showQueryParamsAs)

  /** The method defined in a fixed method input in this endpoint, if any (using e.g. [[EndpointInputsOps.get]] or
    * [[EndpointInputsOps.post]]).
    */
  lazy val method: Option[Method] = {
    import sttp.tapir.internal._
    input.method.orElse(securityInput.method)
  }
}

trait EndpointServerLogicOps[A, I, E, O, -R] { outer: Endpoint[A, I, E, O, R] =>

  /** Combine this public endpoint description with a function, which implements the server-side logic. The logic returns a result, which is
    * either an error or a successful output, wrapped in an effect type `F`. For secure endpoints, use [[serverSecurityLogic]].
    *
    * A server endpoint can be passed to a server interpreter. Each server interpreter supports effects of a specific type(s).
    *
    * Both the endpoint and logic function are considered complete, and cannot be later extended through the returned [[ServerEndpoint]]
    * value (except for endpoint meta-data). Secure endpoints allow providing the security logic before all the inputs and outputs are
    * specified.
    */
  def serverLogic[F[_]](f: I => F[Either[E, O]])(implicit aIsUnit: A =:= Unit): ServerEndpoint.Full[Unit, Unit, I, E, O, R, F] = {
    import sttp.monad.syntax._
    ServerEndpoint.public(this.asInstanceOf[Endpoint[Unit, I, E, O, R]], implicit m => i => f(i).map(x => x))
  }

  /** Like [[serverLogic]], but specialised to the case when the result is always a success (`Right`), hence when the logic type can be
    * simplified to `I => F[O]`.
    */
  def serverLogicSuccess[F[_]](
      f: I => F[O]
  )(implicit aIsUnit: A =:= Unit): ServerEndpoint.Full[Unit, Unit, I, E, O, R, F] =
    ServerEndpoint.public(this.asInstanceOf[Endpoint[Unit, I, E, O, R]], implicit m => i => f(i).map(Right(_)))

  /** Like [[serverLogic]], but specialised to the case when the result is always an error (`Left`), hence when the logic type can be
    * simplified to `I => F[E]`.
    */
  def serverLogicError[F[_]](
      f: I => F[E]
  )(implicit aIsUnit: A =:= Unit): ServerEndpoint.Full[Unit, Unit, I, E, O, R, F] =
    ServerEndpoint.public(this.asInstanceOf[Endpoint[Unit, I, E, O, R]], implicit m => i => f(i).map(Left(_)))

  /** Like [[serverLogic]], but specialised to the case when the logic function is pure, that is doesn't have any side effects. */
  def serverLogicPure[F[_]](f: I => Either[E, O])(implicit aIsUnit: A =:= Unit): ServerEndpoint.Full[Unit, Unit, I, E, O, R, F] =
    ServerEndpoint.public(this.asInstanceOf[Endpoint[Unit, I, E, O, R]], implicit m => i => f(i).unit)

  /** Like [[serverLogic]], but specialised to the case when the result is always a pure success (`Right`), that is doesn't have any side
    * effects.
    */
  def serverLogicSuccessPure[F[_]](f: I => O)(implicit aIsUnit: A =:= Unit): ServerEndpoint.Full[Unit, Unit, I, E, O, R, F] =
    ServerEndpoint.public(this.asInstanceOf[Endpoint[Unit, I, E, O, R]], implicit m => i => f(i).unit.map(Right(_)))

  /** Like [[serverLogic]], but specialised to the case when the result is always a pure error (`Left`), that is doesn't have any side
    * effects.
    */
  def serverLogicErrorPure[F[_]](f: I => E)(implicit aIsUnit: A =:= Unit): ServerEndpoint.Full[Unit, Unit, I, E, O, R, F] =
    ServerEndpoint.public(this.asInstanceOf[Endpoint[Unit, I, E, O, R]], implicit m => i => f(i).unit.map(Left(_)))

  /** Same as [[serverLogic]], but requires `E` to be a throwable, and converts failed effects of type `E` to endpoint errors. */
  def serverLogicRecoverErrors[F[_]](
      f: I => F[O]
  )(implicit eIsThrowable: E <:< Throwable, eClassTag: ClassTag[E], aIsUnit: A =:= Unit): ServerEndpoint.Full[Unit, Unit, I, E, O, R, F] =
    ServerEndpoint.public(this.asInstanceOf[Endpoint[Unit, I, E, O, R]], recoverErrors1[I, E, O, F](f))

  /** Like [[serverLogic]], but specialised to the case when the error type is `Unit` (e.g. a fixed status code), and the result of the
    * logic function is an option. A `None` is then treated as an error response.
    */
  def serverLogicOption[F[_]](
      f: I => F[Option[O]]
  )(implicit aIsUnit: A =:= Unit, eIsUnit: E =:= Unit): ServerEndpoint.Full[Unit, Unit, I, Unit, O, R, F] = {
    import sttp.monad.syntax._
    ServerEndpoint.public(
      this.asInstanceOf[Endpoint[Unit, I, Unit, O, R]],
      implicit m =>
        i =>
          f(i).map {
            case None    => Left(())
            case Some(v) => Right(v)
          }
    )
  }

  //

  /** Combine this endpoint description with a function, which implements the security logic of the endpoint.
    *
    * Subsequently, the endpoint inputs and outputs can be extended (for error outputs, new variants can be added, but they cannot be
    * arbitrarily extended). Then the main server logic can be provided, given a function which accepts as arguments the result of the
    * security logic and the remaining input. The final result is then a [[ServerEndpoint]].
    *
    * A complete server endpoint can be passed to a server interpreter. Each server interpreter supports effects of a specific type(s).
    *
    * An example use-case is defining an endpoint with fully-defined errors, and with security logic built-in. Such an endpoint can be then
    * extended by multiple other endpoints, by specifying different inputs, outputs and the main logic.
    */
  def serverSecurityLogic[PRINCIPAL, F[_]](f: A => F[Either[E, PRINCIPAL]]): PartialServerEndpoint[A, PRINCIPAL, I, E, O, R, F] =
    PartialServerEndpoint(this, _ => f)

  /** Like [[serverSecurityLogic]], but specialised to the case when the result is always a success (`Right`), hence when the logic type can
    * be simplified to `A => F[PRINCIPAL]`.
    */
  def serverSecurityLogicSuccess[PRINCIPAL, F[_]](
      f: A => F[PRINCIPAL]
  ): PartialServerEndpoint[A, PRINCIPAL, I, E, O, R, F] =
    PartialServerEndpoint(this, implicit m => a => f(a).map(Right(_)))

  /** Like [[serverSecurityLogic]], but specialised to the case when the result is always an error (`Left`), hence when the logic type can
    * be simplified to `A => F[E]`.
    */
  def serverSecurityLogicError[PRINCIPAL, F[_]](
      f: A => F[E]
  ): PartialServerEndpoint[A, PRINCIPAL, I, E, O, R, F] =
    PartialServerEndpoint(this, implicit m => a => f(a).map(Left(_)))

  /** Like [[serverSecurityLogic]], but specialised to the case when the logic function is pure, that is doesn't have any side effects. */
  def serverSecurityLogicPure[PRINCIPAL, F[_]](f: A => Either[E, PRINCIPAL]): PartialServerEndpoint[A, PRINCIPAL, I, E, O, R, F] =
    PartialServerEndpoint(this, implicit m => a => f(a).unit)

  /** Same as [[serverSecurityLogic]], but requires `E` to be a throwable, and converts failed effects of type `E` to endpoint errors. */
  def serverSecurityLogicRecoverErrors[PRINCIPAL, F[_]](
      f: A => F[PRINCIPAL]
  )(implicit eIsThrowable: E <:< Throwable, eClassTag: ClassTag[E]): PartialServerEndpoint[A, PRINCIPAL, I, E, O, R, F] =
    PartialServerEndpoint(this, recoverErrors1[A, E, PRINCIPAL, F](f))

  /** Like [[serverSecurityLogic]], but specialised to the case when the error type is `Unit` (e.g. a fixed status code), and the result of
    * the logic function is an option. A `None` is then treated as an error response.
    */
  def serverSecurityLogicOption[PRINCIPAL, F[_]](
      f: A => F[Option[PRINCIPAL]]
  )(implicit eIsUnit: E =:= Unit): PartialServerEndpoint[A, PRINCIPAL, I, Unit, O, R, F] = {
    import sttp.monad.syntax._
    PartialServerEndpoint(
      this.asInstanceOf[Endpoint[A, I, Unit, O, R]],
      implicit m =>
        a =>
          f(a).map {
            case None    => Left(())
            case Some(v) => Right(v)
          }
    )
  }

  //

  /** Like [[serverSecurityLogic]], but allows the security function to contribute to the overall output of the endpoint. A value for the
    * complete output `O` defined so far has to be provided. The value `PRINCIPAL` will be propagated as an input to the regular logic.
    */
  def serverSecurityLogicWithOutput[PRINCIPAL, F[_]](
      f: A => F[Either[E, (O, PRINCIPAL)]]
  ): PartialServerEndpointWithSecurityOutput[A, PRINCIPAL, I, E, O, Unit, R, F] =
    PartialServerEndpointWithSecurityOutput(this.output, this.copy(output = emptyOutput), _ => f)

  /** Like [[serverSecurityLogicWithOutput]], but specialised to the case when the result is always a success (`Right`), hence when the
    * logic type can be simplified to `A => F[(O, PRINCIPAL)]`.
    */
  def serverSecurityLogicSuccessWithOutput[PRINCIPAL, F[_]](
      f: A => F[(O, PRINCIPAL)]
  ): PartialServerEndpointWithSecurityOutput[A, PRINCIPAL, I, E, O, Unit, R, F] =
    PartialServerEndpointWithSecurityOutput(this.output, this.copy(output = emptyOutput), implicit m => a => f(a).map(Right(_)))

  /** Like [[serverSecurityLogicWithOutput]], but specialised to the case when the logic function is pure, that is doesn't have any side
    * effects.
    */
  def serverSecurityLogicPureWithOutput[PRINCIPAL, F[_]](
      f: A => Either[E, (O, PRINCIPAL)]
  ): PartialServerEndpointWithSecurityOutput[A, PRINCIPAL, I, E, O, Unit, R, F] =
    PartialServerEndpointWithSecurityOutput(this.output, this.copy(output = emptyOutput), implicit m => a => f(a).unit)

  /** Same as [[serverSecurityLogicWithOutput]], but requires `E` to be a throwable, and converts failed effects of type `E` to endpoint
    * errors.
    */
  def serverSecurityLogicRecoverErrorsWithOutput[PRINCIPAL, F[_]](
      f: A => F[(O, PRINCIPAL)]
  )(implicit
      eIsThrowable: E <:< Throwable,
      eClassTag: ClassTag[E]
  ): PartialServerEndpointWithSecurityOutput[A, PRINCIPAL, I, E, O, Unit, R, F] =
    PartialServerEndpointWithSecurityOutput(this.output, this.copy(output = emptyOutput), recoverErrors1[A, E, (O, PRINCIPAL), F](f))

  /** Like [[serverSecurityLogicWithOutput]], but specialised to the case when the error type is `Unit` (e.g. a fixed status code), and the
    * result of the logic function is an option. A `None` is then treated as an error response.
    */
  def serverSecurityLogicOptionWithOutput[PRINCIPAL, F[_]](
      f: A => F[Option[(O, PRINCIPAL)]]
  )(implicit eIsUnit: E =:= Unit): PartialServerEndpointWithSecurityOutput[A, PRINCIPAL, I, Unit, O, Unit, R, F] = {
    import sttp.monad.syntax._
    PartialServerEndpointWithSecurityOutput(
      this.output,
      this.copy(output = emptyOutput).asInstanceOf[Endpoint[A, I, Unit, Unit, R]],
      implicit m =>
        a =>
          f(a).map {
            case None    => Left(())
            case Some(v) => Right(v)
          }
    )
  }

  // Direct-style

  /** Direct-style variant of [[serverLogic]], using the [[Identity]] "effect". */
  def handle(f: I => Either[E, O])(implicit aIsUnit: A =:= Unit): ServerEndpoint.Full[Unit, Unit, I, E, O, R, Identity] =
    serverLogic[Identity](f)

  /** Direct-style variant of [[serverLogicSuccess]], using the [[Identity]] "effect". */
  def handleSuccess(f: I => O)(implicit aIsUnit: A =:= Unit): ServerEndpoint.Full[Unit, Unit, I, E, O, R, Identity] =
    serverLogicSuccess[Identity](f)

  /** Direct-style variant of [[serverLogicError]], using the [[Identity]] "effect". */
  def handleError(f: I => E)(implicit aIsUnit: A =:= Unit): ServerEndpoint.Full[Unit, Unit, I, E, O, R, Identity] =
    serverLogicError[Identity](f)

  /** Direct-style variant of [[serverLogicRecoverErrors]], using the [[Identity]] "effect". */
  def handleRecoverErrors(
      f: I => O
  )(implicit
      eIsThrowable: E <:< Throwable,
      eClassTag: ClassTag[E],
      aIsUnit: A =:= Unit
  ): ServerEndpoint.Full[Unit, Unit, I, E, O, R, Identity] =
    serverLogicRecoverErrors[Identity](f)

  /** Direct-style variant of [[serverLogicOption]], using the [[Identity]] "effect". */
  def handleOption(
      f: I => Option[O]
  )(implicit aIsUnit: A =:= Unit, eIsUnit: E =:= Unit): ServerEndpoint.Full[Unit, Unit, I, Unit, O, R, Identity] =
    serverLogicOption[Identity](f)

  //

  /** Direct-style variant of [[serverSecurityLogic]], using the [[Identity]] "effect". */
  def handleSecurity[PRINCIPAL](f: A => Either[E, PRINCIPAL]): PartialServerEndpointSync[A, PRINCIPAL, I, E, O, R] =
    PartialServerEndpointSync(this, f)

  /** Direct-style variant of [[serverSecurityLogicSuccess]], using the [[Identity]] "effect". */
  def handleSecuritySuccess[PRINCIPAL](f: A => PRINCIPAL): PartialServerEndpointSync[A, PRINCIPAL, I, E, O, R] =
    PartialServerEndpointSync(this, a => Right(f(a)))

  /** Direct-style variant of [[serverSecurityLogicError]], using the [[Identity]] "effect". */
  def handleSecurityError[PRINCIPAL](f: A => E): PartialServerEndpointSync[A, PRINCIPAL, I, E, O, R] =
    PartialServerEndpointSync(this, a => Left(f(a)))

  /** Direct-style variant of [[serverSecurityLogicRecoverErrors]], using the [[Identity]] "effect". */
  def handleSecurityRecoverErrors[PRINCIPAL](
      f: A => PRINCIPAL
  )(implicit eIsThrowable: E <:< Throwable, eClassTag: ClassTag[E]): PartialServerEndpointSync[A, PRINCIPAL, I, E, O, R] =
    PartialServerEndpointSync(this, recoverErrors1[A, E, PRINCIPAL, Identity](f)(implicitly, implicitly)(IdentityMonad))

  /** Direct-style variant of [[serverSecurityLogicOption]], using the [[Identity]] "effect". */
  def handleSecurityOption[PRINCIPAL](f: A => Option[PRINCIPAL])(implicit
      eIsUnit: E =:= Unit
  ): PartialServerEndpointSync[A, PRINCIPAL, I, Unit, O, R] = {
    PartialServerEndpointSync(
      this.asInstanceOf[Endpoint[A, I, Unit, O, R]],
      a =>
        f(a) match {
          case None    => Left(())
          case Some(v) => Right(v)
        }
    )
  }

  //

  /** Direct-style variant of [[serverSecurityLogicWithOutput]], using the [[Identity]] "effect". */
  def handleSecurityWithOutput[PRINCIPAL](
      f: A => Either[E, (O, PRINCIPAL)]
  ): PartialServerEndpointWithSecurityOutputSync[A, PRINCIPAL, I, E, O, Unit, R] =
    PartialServerEndpointWithSecurityOutputSync(this.output, this.copy(output = emptyOutput), f)

  /** Direct-style variant of [[serverSecurityLogicSuccessWithOutput]], using the [[Identity]] "effect". */
  def handleSecuritySuccessWithOutput[PRINCIPAL](
      f: A => (O, PRINCIPAL)
  ): PartialServerEndpointWithSecurityOutputSync[A, PRINCIPAL, I, E, O, Unit, R] =
    PartialServerEndpointWithSecurityOutputSync(this.output, this.copy(output = emptyOutput), a => Right(f(a)))

  /** Direct-style variant of [[serverSecurityLogicRecoverErrorsWithOutput]], using the [[Identity]] "effect". */
  def handleSecurityRecoverErrorsWithOutput[PRINCIPAL](
      f: A => (O, PRINCIPAL)
  )(implicit
      eIsThrowable: E <:< Throwable,
      eClassTag: ClassTag[E]
  ): PartialServerEndpointWithSecurityOutputSync[A, PRINCIPAL, I, E, O, Unit, R] =
    PartialServerEndpointWithSecurityOutputSync(
      this.output,
      this.copy(output = emptyOutput),
      recoverErrors1[A, E, (O, PRINCIPAL), Identity](f)(implicitly, implicitly)(IdentityMonad)
    )

  /** Direct-style variant of [[serverSecurityLogicOptionWithOutput]], using the [[Identity]] "effect". */
  def handleSecurityOptionWithOutput[PRINCIPAL](
      f: A => Option[(O, PRINCIPAL)]
  )(implicit eIsUnit: E =:= Unit): PartialServerEndpointWithSecurityOutputSync[A, PRINCIPAL, I, Unit, O, Unit, R] = {
    PartialServerEndpointWithSecurityOutputSync(
      this.output,
      this.copy(output = emptyOutput).asInstanceOf[Endpoint[A, I, Unit, Unit, R]],
      a =>
        f(a) match {
          case None    => Left(())
          case Some(v) => Right(v)
        }
    )
  }
}

case class EndpointInfo(
    name: Option[String],
    summary: Option[String],
    description: Option[String],
    tags: Vector[String],
    deprecated: Boolean,
    attributes: AttributeMap
) {
  def name(n: String): EndpointInfo = this.copy(name = Some(n))
  def summary(s: String): EndpointInfo = copy(summary = Some(s))
  def description(d: String): EndpointInfo = copy(description = Some(d))
  def deprecated(d: Boolean): EndpointInfo = copy(deprecated = d)
  def attribute[T](k: AttributeKey[T]): Option[T] = attributes.get(k)
  def attribute[T](k: AttributeKey[T], v: T): EndpointInfo = copy(attributes = attributes.put(k, v))

  /** Append to the existing tags * */
  def tags(ts: List[String]): EndpointInfo = copy(tags = tags ++ ts)
  def tag(t: String): EndpointInfo = copy(tags = tags :+ t)

  /** Overwrite the existing tags * */
  def withTags(ts: List[String]): EndpointInfo = copy(tags = ts.toVector)
  def withTag(t: String): EndpointInfo = copy(tags = Vector(t))
  def withoutTags: EndpointInfo = copy(tags = Vector.empty)
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy