Defining Server-Side Logic for an Endpoint: Three Approaches

The translation of the article was prepared in anticipation of the start of the "Scala developer" course








, tapir HTTP. API, , , , .



, ( , , ), . , tapir!



.





, .





, . , . baseEndpoint, : . , , , .



import java.util.UUID

import sttp.tapir._
import sttp.tapir.json.circe._
import io.circe.generic.auto._
import sttp.model.StatusCode

case class AuthToken(token: String)
case class Error(msg: String, statusCode: StatusCode) extends Exception

val error: EndpointOutput[Error] = stringBody.and(statusCode).mapTo(Error)
val baseEndpoint: Endpoint[AuthToken, Error, Unit, Nothing] = endpoint
  .in(header[String]("X-Authorization")
      .description("Only authorized users can add pets")
      .example("1234")
      .mapTo(AuthToken))
  .in("api" / "1.0")
  .errorOut(error)


, . -:



case class User(id: UUID, name: String)
case class Pet(id: UUID, kind: String, name: String)

val getPet: Endpoint[(AuthToken, UUID), Error, Pet, Nothing] =
  baseEndpoint
    .get
    .in(query[UUID]("id").description("The id of the pet to find"))
    .out(jsonBody[Pet])
    .description("Finds a pet by id")

val addPet: Endpoint[(AuthToken, Pet), Error, Unit, Nothing] =
  baseEndpoint
    .post
    .in(jsonBody[Pet])
    .description("Adds a pet")


, , . , . Future -, - Future.



import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global

//    Error,    
def authorize(authToken: AuthToken): Future[User] = ???
def findPetForUser(user: User, id: UUID): Future[Option[Pet]] = ???
def addPetToUser(user: User, pet: Pet): Future[Unit] = ???

val getPetWithLogic = getPet.serverLogicRecoverErrors {
  case (authToken, id) =>
    authorize(authToken).flatMap { user =>
      findPetForUser(user, id).flatMap {
        case Some(pet) => Future.successful(pet)
        case None      => Future.failed(Error("Not found", StatusCode.NotFound))
      }
    }
}

val addPetWithLogic = addPet.serverLogicRecoverErrors {
  case (authToken, pet) =>
    authorize(authToken).flatMap { user =>
      addPetToUser(user, pet)
    }
}


, , , Exception, - , , .



, , , akka-http Route, akka-http .



//      
akka.http.scaladsl.Route
import akka.http.scaladsl.server.Route
import sttp.tapir.server.akkahttp._
val routes: Route = List(getPetWithLogic, addPetWithLogic).toRoute

//  , 
akka-http




. , , . , Authorization. , , . , , , .



import java.util.UUID

import cats.effect.{ContextShift, IO, Timer}
import sttp.tapir._
import sttp.model.StatusCode

case class AuthToken(token: String)
case class Error(msg: String, statusCode: StatusCode)
case class User(id: UUID, name: String)
case class Pet(id: UUID, kind: String, name: String)

def authorize(authToken: AuthToken): IO[Either[Error, User]] = ???
def findPetForUser(user: User, id: UUID): IO[Either[Error, Option[Pet]]] = ???
def addPetToUser(user: User, pet: Pet): IO[Either[Error, Unit]] = ???


,



-. , ( : serverLogicForCurrent), ( ):



import sttp.tapir.json.circe._
import io.circe.generic.auto._
import sttp.tapir.server.PartialServerEndpoint

val error: EndpointOutput[Error] = stringBody.and(statusCode).mapTo(Error)
val secureEndpoint: PartialServerEndpoint[User, Unit, Error, Unit, Nothing, IO] = 
  endpoint
    .in(auth.bearer[String].mapTo(AuthToken))
    .in("api" / "1.0")
    .errorOut(error)
    .serverLogicForCurrent(authorize)


, PartialServerEndpoint. / ( , ).



! , , , — , .



, /, , , : (: User) .



val getPetWithLogic =
  secureEndpoint
    .get
    .in(query[UUID]("id").description("The id of the pet to find"))
    .out(jsonBody[Pet])
    .description("Finds a pet by id")
    .serverLogic {
      case (user, id) =>
        findPetForUser(user, id).map {
          case Right(Some(pet)) => Right(pet)
          case Right(None)      => Left(Error("Not found", StatusCode.NotFound))
          case Left(error)      => Left(error)
        }
    }

val addPetWithLogic =
  secureEndpoint
    .post
    .in(jsonBody[Pet])
    .description("Adds a pet")
    .serverLogic((addPetToUser _).tupled)


, , , case . , IO . , IO[Either[Error, _]].



ServerEndpoint ( , ). , , IO , http4s.



// the endpoints are interpreted as an http4s.HttpRoutes[IO]
import sttp.tapir.server.http4s._
import org.http4s.HttpRoutes
implicit val ec: ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global
implicit val contextShift: ContextShift[IO] = IO.contextShift(ec)
implicit val timer: Timer[IO] = IO.timer(ec)
val routes: HttpRoutes[IO] = List(getPetWithLogic, addPetWithLogic).toRoutes

// expose routes using http4s




tapir , . , tapir-core tapir-json-circe.



- - . . . , (, cookies):



import java.util.UUID
import io.circe.generic.auto._
import sttp.model.StatusCode
import sttp.tapir._
import sttp.tapir.json.circe._

object Endpoints {
  case class AuthToken(token: String)
  case class Error(msg: String, statusCode: StatusCode) extends Exception
  case class User(id: UUID, name: String)
  case class Pet(id: UUID, kind: String, name: String)

  val error: EndpointOutput[Error] = stringBody.and(statusCode).mapTo(Error)
  val baseEndpoint: Endpoint[AuthToken, Error, Unit, Nothing] = 
    endpoint
      .in(auth.apiKey(cookie[String]("Token")).mapTo(AuthToken))
      .in("api" / "1.0")
      .errorOut(error)

  val getPet: Endpoint[(AuthToken, UUID), Error, Pet, Nothing] =
    baseEndpoint
      .get
      .in(query[UUID]("id").description("The id of the pet to find"))
      .out(jsonBody[Pet])
      .description("Finds a pet by id")

  val addPet: Endpoint[(AuthToken, Pet), Error, Unit, Nothing] =
    baseEndpoint
      .post
      .in(jsonBody[Pet])
      .description("Adds a pet")
}


, , . .



( ), serverLogicPart, ( ), .



serverLogicPart , ( , ). ServerEndpointInParts .



, , , , ( ) , , .



, - , , User:



object Server {
  import Endpoints._
  import scala.concurrent.Future
  import scala.concurrent.ExecutionContext.Implicits.global

  // should fail with Error if user not found
  def authorize(authToken: AuthToken): Future[Either[Error, User]] = ???
  def findPetForUser(user: User, id: UUID): Future[Either[Error, Option[Pet]]] = ???
  def addPetToUser(user: User, pet: Pet): Future[Either[Error, Unit]] = ???

  val getPetWithLogic = getPet.serverLogicPart(authorize).andThen {
    case (user, id) =>
      findPetForUser(user, id).map {
        case Right(Some(pet)) => Right(pet)
        case Right(None)      => Left(Error("Not found", StatusCode.NotFound))
        case Left(error)      => Left(error)
      }
  }

  val addPetWithLogic = addPet.serverLogicPart(authorize)
    .andThen((addPetToUser _).tupled)

  // the endpoints are now interpreted as an akka.http.scaladsl.Route
  import akka.http.scaladsl.server.Route
  import sttp.tapir.server.akkahttp._
  val routes: Route = List(getPetWithLogic, addPetWithLogic).toRoute

  //  ,  akka-http
}


, ServerEndpoint. , Future akka-http.





:



  • .
  • , . / ( ), .
  • , . , .


, , . . , , .



. , , , .



, , tapir, HTTP!






«Scala-»







All Articles