Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package sttp.tapir.client.sttp
import sttp.capabilities.Streams
import sttp.client3._
import sttp.model._
import sttp.shared.Identity
import sttp.tapir.Codec.PlainCodec
import sttp.tapir._
import sttp.tapir.client.ClientOutputParams
Expand Down
109 changes: 107 additions & 2 deletions core/src/main/scala/sttp/tapir/Endpoint.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ 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, PartialServerEndpointWithSecurityOutput, ServerEndpoint}
import sttp.tapir.server.{PartialServerEndpoint, PartialServerEndpointSync, PartialServerEndpointWithSecurityOutput, PartialServerEndpointWithSecurityOutputSync, ServerEndpoint}
import sttp.tapir.typelevel.{ErasureSameAsType, ParamConcat}

import scala.reflect.ClassTag
Expand All @@ -23,7 +25,8 @@ import scala.reflect.ClassTag
* 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.
* 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.
*
Expand Down Expand Up @@ -582,6 +585,108 @@ trait EndpointServerLogicOps[A, I, E, O, -R] { outer: Endpoint[A, I, E, O, R] =>
}
)
}

// 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(
Expand Down
139 changes: 139 additions & 0 deletions core/src/main/scala/sttp/tapir/server/PartialServerEndpointSync.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package sttp.tapir.server

import sttp.shared.Identity
import sttp.tapir._
import sttp.tapir.internal._

import scala.reflect.ClassTag

/** Direct-style variant of [[PartialServerEndpoint]], using the [[Identity]] "effect". */
case class PartialServerEndpointSync[SECURITY_INPUT, PRINCIPAL, INPUT, ERROR_OUTPUT, OUTPUT, -R](
endpoint: Endpoint[SECURITY_INPUT, INPUT, ERROR_OUTPUT, OUTPUT, R],
securityLogic: SECURITY_INPUT => Either[ERROR_OUTPUT, PRINCIPAL]
) extends EndpointInputsOps[SECURITY_INPUT, INPUT, ERROR_OUTPUT, OUTPUT, R]
with EndpointOutputsOps[SECURITY_INPUT, INPUT, ERROR_OUTPUT, OUTPUT, R]
with EndpointErrorOutputVariantsOps[SECURITY_INPUT, INPUT, ERROR_OUTPUT, OUTPUT, R]
with EndpointInfoOps[R]
with EndpointMetaOps { outer =>
override type ThisType[-_R] = PartialServerEndpointSync[SECURITY_INPUT, PRINCIPAL, INPUT, ERROR_OUTPUT, OUTPUT, _R]
override type EndpointType[_A, _I, _E, _O, -_R] = PartialServerEndpointSync[_A, PRINCIPAL, _I, _E, _O, _R]

override def securityInput: EndpointInput[SECURITY_INPUT] = endpoint.securityInput
override def input: EndpointInput[INPUT] = endpoint.input
override def errorOutput: EndpointOutput[ERROR_OUTPUT] = endpoint.errorOutput
override def output: EndpointOutput[OUTPUT] = endpoint.output
override def info: EndpointInfo = endpoint.info

override private[tapir] def withInput[I2, R2](
input: EndpointInput[I2]
): PartialServerEndpointSync[SECURITY_INPUT, PRINCIPAL, I2, ERROR_OUTPUT, OUTPUT, R with R2] =
copy(endpoint = endpoint.copy(input = input))
override private[tapir] def withOutput[O2, R2](output: EndpointOutput[O2]) = copy(endpoint = endpoint.copy(output = output))
override private[tapir] def withErrorOutputVariant[E2, R2](
errorOutput: EndpointOutput[E2],
embedE: ERROR_OUTPUT => E2
): PartialServerEndpointSync[SECURITY_INPUT, PRINCIPAL, INPUT, E2, OUTPUT, R with R2] =
this.copy(
endpoint = endpoint.copy(errorOutput = errorOutput),
securityLogic = a =>
securityLogic(a) match {
case Left(e) => Left(embedE(e))
case Right(o) => Right(o)
}
)
override private[tapir] def withInfo(info: EndpointInfo) = copy(endpoint = endpoint.copy(info = info))

override protected def showType: String = "PartialServerEndpoint"

def handle(
f: PRINCIPAL => INPUT => Either[ERROR_OUTPUT, OUTPUT]
): ServerEndpoint.Full[SECURITY_INPUT, PRINCIPAL, INPUT, ERROR_OUTPUT, OUTPUT, R, Identity] =
ServerEndpoint[SECURITY_INPUT, PRINCIPAL, INPUT, ERROR_OUTPUT, OUTPUT, R, Identity](endpoint, _ => securityLogic, _ => f)

def handleSuccess(
f: PRINCIPAL => INPUT => OUTPUT
): ServerEndpoint.Full[SECURITY_INPUT, PRINCIPAL, INPUT, ERROR_OUTPUT, OUTPUT, R, Identity] =
ServerEndpoint[SECURITY_INPUT, PRINCIPAL, INPUT, ERROR_OUTPUT, OUTPUT, R, Identity](
endpoint,
_ => securityLogic,
_ => u => i => Right(f(u)(i))
)

def handleError(
f: PRINCIPAL => INPUT => ERROR_OUTPUT
): ServerEndpoint.Full[SECURITY_INPUT, PRINCIPAL, INPUT, ERROR_OUTPUT, OUTPUT, R, Identity] =
ServerEndpoint[SECURITY_INPUT, PRINCIPAL, INPUT, ERROR_OUTPUT, OUTPUT, R, Identity](
endpoint,
_ => securityLogic,
_ => u => i => Left(f(u)(i))
)

def handleRecoverErrors(
f: PRINCIPAL => INPUT => OUTPUT
)(implicit
eIsThrowable: ERROR_OUTPUT <:< Throwable,
eClassTag: ClassTag[ERROR_OUTPUT]
): ServerEndpoint.Full[SECURITY_INPUT, PRINCIPAL, INPUT, ERROR_OUTPUT, OUTPUT, R, Identity] =
ServerEndpoint[SECURITY_INPUT, PRINCIPAL, INPUT, ERROR_OUTPUT, OUTPUT, R, Identity](
endpoint,
_ => securityLogic,
recoverErrors2[PRINCIPAL, INPUT, ERROR_OUTPUT, OUTPUT, Identity](f)
)

def handleOption(f: PRINCIPAL => INPUT => Option[OUTPUT])(implicit
eIsUnit: ERROR_OUTPUT =:= Unit
): ServerEndpoint.Full[SECURITY_INPUT, PRINCIPAL, INPUT, Unit, OUTPUT, R, Identity] =
ServerEndpoint[SECURITY_INPUT, PRINCIPAL, INPUT, Unit, OUTPUT, R, Identity](
endpoint.asInstanceOf[Endpoint[SECURITY_INPUT, INPUT, Unit, OUTPUT, R]],
_ => securityLogic.asInstanceOf[SECURITY_INPUT => Either[Unit, PRINCIPAL]],
_ =>
u =>
i =>
f(u)(i) match {
case None => Left(())
case Some(v) => Right(v)
}
)

/** If the error type is an `Either`, e.g. when using `errorOutEither`, this method accepts server logic that returns either success or
* the `Right` error type. Use of this method avoids having to wrap the returned error in `Right`.
*/
def handleRightErrorOrSuccess[LE, RE](
f: PRINCIPAL => INPUT => Either[RE, OUTPUT]
)(implicit
eIsEither: Either[LE, RE] =:= ERROR_OUTPUT
): ServerEndpoint.Full[SECURITY_INPUT, PRINCIPAL, INPUT, ERROR_OUTPUT, OUTPUT, R, Identity] =
ServerEndpoint[SECURITY_INPUT, PRINCIPAL, INPUT, ERROR_OUTPUT, OUTPUT, R, Identity](
endpoint,
_ => securityLogic,
_ =>
u =>
i => {
f(u)(i) match {
case Left(e) => Left(Right(e))
case Right(r) => Right(r)
}
}
)

/** If the error type is an `Either`, e.g. when using `errorOutEither`, this method accepts server logic that returns either success or
* the `Left` error type. Use of this method avoids having to wrap the returned error in `Left`.
*/
def handleLeftErrorOrSuccess[LE, RE](
f: PRINCIPAL => INPUT => Either[LE, OUTPUT]
)(implicit
eIsEither: Either[LE, RE] =:= ERROR_OUTPUT
): ServerEndpoint.Full[SECURITY_INPUT, PRINCIPAL, INPUT, ERROR_OUTPUT, OUTPUT, R, Identity] =
ServerEndpoint[SECURITY_INPUT, PRINCIPAL, INPUT, ERROR_OUTPUT, OUTPUT, R, Identity](
endpoint,
_ => securityLogic,
_ =>
u =>
i => {
f(u)(i) match {
case Left(e) => Left(Left(e))
case Right(r) => Right(r)
}
}
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package sttp.tapir.server

import sttp.monad.IdentityMonad
import sttp.shared.Identity
import sttp.tapir._
import sttp.tapir.internal._

import scala.reflect.ClassTag

/** Direct-style variant of [[PartialServerEndpointWithSecurityOutput]], using the [[Identity]] "effect". */
case class PartialServerEndpointWithSecurityOutputSync[SECURITY_INPUT, PRINCIPAL, INPUT, ERROR_OUTPUT, SECURITY_OUTPUT, OUTPUT, -R](
securityOutput: EndpointOutput[SECURITY_OUTPUT],
endpoint: Endpoint[SECURITY_INPUT, INPUT, ERROR_OUTPUT, OUTPUT, R],
securityLogic: SECURITY_INPUT => Either[ERROR_OUTPUT, (SECURITY_OUTPUT, PRINCIPAL)]
) extends EndpointInputsOps[SECURITY_INPUT, INPUT, ERROR_OUTPUT, OUTPUT, R]
with EndpointOutputsOps[SECURITY_INPUT, INPUT, ERROR_OUTPUT, OUTPUT, R]
with EndpointErrorOutputVariantsOps[SECURITY_INPUT, INPUT, ERROR_OUTPUT, OUTPUT, R]
with EndpointInfoOps[R]
with EndpointMetaOps { outer =>
override type ThisType[-_R] =
PartialServerEndpointWithSecurityOutputSync[SECURITY_INPUT, PRINCIPAL, INPUT, ERROR_OUTPUT, SECURITY_OUTPUT, OUTPUT, _R]
override type EndpointType[_A, _I, _E, _O, -_R] =
PartialServerEndpointWithSecurityOutputSync[_A, PRINCIPAL, _I, _E, SECURITY_OUTPUT, _O, _R]

override def securityInput: EndpointInput[SECURITY_INPUT] = endpoint.securityInput
override def input: EndpointInput[INPUT] = endpoint.input
override def errorOutput: EndpointOutput[ERROR_OUTPUT] = endpoint.errorOutput
override def output: EndpointOutput[OUTPUT] = endpoint.output
override def info: EndpointInfo = endpoint.info

override private[tapir] def withInput[I2, R2](
input: EndpointInput[I2]
): PartialServerEndpointWithSecurityOutputSync[SECURITY_INPUT, PRINCIPAL, I2, ERROR_OUTPUT, SECURITY_OUTPUT, OUTPUT, R with R2] =
copy(endpoint = endpoint.copy(input = input))
override private[tapir] def withOutput[O2, R2](output: EndpointOutput[O2]) = copy(endpoint = endpoint.copy(output = output))
override private[tapir] def withErrorOutputVariant[E2, R2](
errorOutput: EndpointOutput[E2],
embedE: ERROR_OUTPUT => E2
): PartialServerEndpointWithSecurityOutputSync[SECURITY_INPUT, PRINCIPAL, INPUT, E2, SECURITY_OUTPUT, OUTPUT, R with R2] =
this.copy(
endpoint = endpoint.copy(errorOutput = errorOutput),
securityLogic = a =>
securityLogic(a) match {
case Left(e) => Left(embedE(e))
case Right(o) => Right(o)
}
)
override private[tapir] def withInfo(info: EndpointInfo) = copy(endpoint = endpoint.copy(info = info))

override protected def showType: String = "PartialServerEndpointWithSecurityOutput"

def handle(
f: PRINCIPAL => INPUT => Either[ERROR_OUTPUT, OUTPUT]
): ServerEndpoint.Full[SECURITY_INPUT, (SECURITY_OUTPUT, PRINCIPAL), INPUT, ERROR_OUTPUT, (SECURITY_OUTPUT, OUTPUT), R, Identity] =
ServerEndpoint[SECURITY_INPUT, (SECURITY_OUTPUT, PRINCIPAL), INPUT, ERROR_OUTPUT, (SECURITY_OUTPUT, OUTPUT), R, Identity](
endpoint.prependOut(securityOutput),
_ => securityLogic,
_ => so_u => i => f(so_u._2)(i).right.map(o => (so_u._1, o))
)

def handleSuccess(
f: PRINCIPAL => INPUT => OUTPUT
): ServerEndpoint.Full[SECURITY_INPUT, (SECURITY_OUTPUT, PRINCIPAL), INPUT, ERROR_OUTPUT, (SECURITY_OUTPUT, OUTPUT), R, Identity] =
ServerEndpoint[SECURITY_INPUT, (SECURITY_OUTPUT, PRINCIPAL), INPUT, ERROR_OUTPUT, (SECURITY_OUTPUT, OUTPUT), R, Identity](
endpoint.prependOut(securityOutput),
_ => securityLogic,
_ => so_u => i => Right((so_u._1, f(so_u._2)(i)))
)

def handleError(
f: PRINCIPAL => INPUT => ERROR_OUTPUT
): ServerEndpoint.Full[SECURITY_INPUT, (SECURITY_OUTPUT, PRINCIPAL), INPUT, ERROR_OUTPUT, (SECURITY_OUTPUT, OUTPUT), R, Identity] =
ServerEndpoint[SECURITY_INPUT, (SECURITY_OUTPUT, PRINCIPAL), INPUT, ERROR_OUTPUT, (SECURITY_OUTPUT, OUTPUT), R, Identity](
endpoint.prependOut(securityOutput),
_ => securityLogic,
_ => so_u => i => Left(f(so_u._2)(i))
)

def handleRecoverErrors(
f: PRINCIPAL => INPUT => OUTPUT
)(implicit
eIsThrowable: ERROR_OUTPUT <:< Throwable,
eClassTag: ClassTag[ERROR_OUTPUT]
): ServerEndpoint.Full[SECURITY_INPUT, (SECURITY_OUTPUT, PRINCIPAL), INPUT, ERROR_OUTPUT, (SECURITY_OUTPUT, OUTPUT), R, Identity] =
ServerEndpoint[SECURITY_INPUT, (SECURITY_OUTPUT, PRINCIPAL), INPUT, ERROR_OUTPUT, (SECURITY_OUTPUT, OUTPUT), R, Identity](
endpoint.prependOut(securityOutput),
_ => securityLogic,
_ =>
recoverErrors2[(SECURITY_OUTPUT, PRINCIPAL), INPUT, ERROR_OUTPUT, (SECURITY_OUTPUT, OUTPUT), Identity](so_u =>
i => (so_u._1, f(so_u._2)(i))
)(
implicitly,
implicitly
)(IdentityMonad)
)

def handleOption(f: PRINCIPAL => INPUT => Option[OUTPUT])(implicit
eIsUnit: ERROR_OUTPUT =:= Unit
): ServerEndpoint.Full[SECURITY_INPUT, (SECURITY_OUTPUT, PRINCIPAL), INPUT, Unit, (SECURITY_OUTPUT, OUTPUT), R, Identity] =
ServerEndpoint[SECURITY_INPUT, (SECURITY_OUTPUT, PRINCIPAL), INPUT, Unit, (SECURITY_OUTPUT, OUTPUT), R, Identity](
endpoint.prependOut(securityOutput).asInstanceOf[Endpoint[SECURITY_INPUT, INPUT, Unit, (SECURITY_OUTPUT, OUTPUT), R]],
_ => securityLogic.asInstanceOf[SECURITY_INPUT => Identity[Either[Unit, (SECURITY_OUTPUT, PRINCIPAL)]]],
_ =>
so_u =>
i =>
f(so_u._2)(i) match {
case None => Left(())
case Some(v) => Right((so_u._1, v))
}
)
}
Loading