Skip to content

Commit

Permalink
Add Endpoint serverLogicSuccessPure and serverLogicErrorPure (#3837)
Browse files Browse the repository at this point in the history
  • Loading branch information
geirolz authored Jun 14, 2024
1 parent 4a24c64 commit f5cdf32
Show file tree
Hide file tree
Showing 4 changed files with 35 additions and 13 deletions.
26 changes: 24 additions & 2 deletions core/src/main/scala/sttp/tapir/Endpoint.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@ 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.server.{
PartialServerEndpoint,
PartialServerEndpointSync,
PartialServerEndpointWithSecurityOutput,
PartialServerEndpointWithSecurityOutputSync,
ServerEndpoint
}
import sttp.tapir.typelevel.{ErasureSameAsType, ParamConcat}

import scala.reflect.ClassTag
Expand Down Expand Up @@ -446,6 +452,18 @@ trait EndpointServerLogicOps[A, I, E, O, -R] { outer: Endpoint[A, I, E, O, R] =>
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]
Expand Down Expand Up @@ -603,7 +621,11 @@ trait EndpointServerLogicOps[A, I, E, O, -R] { outer: Endpoint[A, I, E, O, R] =>
/** 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] =
)(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". */
Expand Down
12 changes: 6 additions & 6 deletions docs/redoc/src/main/scala/sttp/tapir/redoc/Redoc.scala
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ object Redoc {
else if (specNameLowerCase.endsWith(".yaml") || specNameLowerCase.endsWith(".yml")) MediaType("text", "yaml")
else MediaType("text", "plain")

val specEndpoint = contentEndpoint(specName, specMediaType).serverLogicPure[F](_ => Right(spec))
val specEndpoint = contentEndpoint(specName, specMediaType).serverLogicSuccessPure[F](_ => spec)

val specPrefix = if (options.useRelativePaths) "." else "/" + (options.contextPath ++ options.pathPrefix).mkString("/")
val html: String = redocHtml(
Expand All @@ -77,26 +77,26 @@ object Redoc {
options.redocOptions,
options.redocThemeOptionsJson
)
val htmlEndpoint = contentEndpoint(htmlName, MediaType.TextHtml).serverLogicPure[F](_ => Right(html))
val htmlEndpoint = contentEndpoint(htmlName, MediaType.TextHtml).serverLogicSuccessPure[F](_ => html)

val lastSegmentInput: EndpointInput[Option[String]] = extractFromRequest(_.uri.path.lastOption)

val redirectToHtmlEndpoint =
baseEndpoint
.in(lastSegmentInput)
.out(redirectOutput)
.serverLogicPure[F] { lastSegment =>
.serverLogicSuccessPure[F] { lastSegment =>
if (options.useRelativePaths) {
val pathFromLastSegment: String = lastSegment match {
case Some(s) if s.nonEmpty => s + "/"
case _ => ""
}
Right(s"./$pathFromLastSegment$htmlName")
s"./$pathFromLastSegment$htmlName"
} else
Right(options.contextPath ++ options.pathPrefix match {
options.contextPath ++ options.pathPrefix match {
case Nil => s"/$htmlName"
case segments => s"/${segments.mkString("/")}/$htmlName"
})
}
}

List(specEndpoint, htmlEndpoint, redirectToHtmlEndpoint)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ object SwaggerUI {
// #2396: although application/yaml is not official, that's what swagger ui sends in the accept header
override def mediaType: MediaType = MediaType("application", "yaml")
})))
.serverLogicPure[F](_ => Right(yaml))
.serverLogicSuccessPure[F](_ => yaml)

// swagger-ui webjar comes with the petstore pre-configured; this cannot be changed at runtime
// (see https://github.com/softwaremill/tapir/issues/1695), hence replacing the address in the served document
Expand All @@ -69,7 +69,7 @@ object SwaggerUI {

val textJavascriptUtf8: EndpointIO.Body[String, String] = stringBodyUtf8AnyFormat(Codec.string.format(CodecFormat.TextJavascript()))
val swaggerInitializerJsEndpoint =
baseEndpoint.in("swagger-initializer.js").out(textJavascriptUtf8).serverLogicPure[F](_ => Right(swaggerInitializerJsWithOptions))
baseEndpoint.in("swagger-initializer.js").out(textJavascriptUtf8).serverLogicSuccessPure[F](_ => swaggerInitializerJsWithOptions)

val resourcesEndpoint = staticResourcesGetServerEndpoint[F](prefixInput)(
SwaggerUI.getClass.getClassLoader,
Expand All @@ -84,10 +84,10 @@ object SwaggerUI {
.in(queryParams)
.in(lastSegmentInput)
.out(redirectOutput)
.serverLogicPure[F] { case (params, lastSegment) =>
.serverLogicSuccessPure[F] { case (params, lastSegment) =>
val queryString = if (params.toSeq.nonEmpty) s"?${params.toString}" else ""
val path = if (options.useRelativePaths) lastSegment.map(str => s"$str/").getOrElse("") else ""
Right(s"${concat(fullPathPrefix, path + queryString)}")
s"${concat(fullPathPrefix, path + queryString)}"
}

List(yamlEndpoint, redirectToSlashEndpoint, swaggerInitializerJsEndpoint, resourcesEndpoint)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ class FilterServerEndpointsTest extends AnyFlatSpec with Matchers {
}

implicit class NoLogic[I, E](e: PublicEndpoint[I, E, Unit, Any]) {
def noLogic: ServerEndpoint[Any, Future] = e.serverLogicPure[Future](_ => Right(()))
def noLogic: ServerEndpoint[Any, Future] = e.serverLogicSuccessPure[Future](_ => ())
}

def requestWithPath(path: String): ServerRequest = new ServerRequest {
Expand Down

0 comments on commit f5cdf32

Please sign in to comment.