Skip to content

Commit

Permalink
Move Id alias to core. Add serverLogicSync & other variants to Endpoi…
Browse files Browse the repository at this point in the history
…nt. (#3789)
  • Loading branch information
adamw authored May 28, 2024
1 parent 72fb4d5 commit ed7b60e
Show file tree
Hide file tree
Showing 71 changed files with 658 additions and 367 deletions.
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

0 comments on commit ed7b60e

Please sign in to comment.