[Scala, Функциональное программирование] Определение серверной логики для конечной точки: три подхода (перевод)
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Перевод статьи подготовлен в преддверии старта курса «Scala-разработчик»
Теа, Ральф и Джесси используют 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-разработчик»
===========
Источник:
habr.com
===========
===========
Автор оригинала: Adam Warski
===========Похожие новости:
- [1С-Битрикс] Что должен уметь программист 1C?
- [Java, Kotlin] Производитель/потребитель на Kafka и Kotlin (перевод)
- [Apache, Big Data] Распределенное обучение с Apache MXNet и Horovod (перевод)
- [PHP, Symfony] Управление секретами в Symfony (перевод)
- [Виртуализация, Настройка Linux] Сравниваем лучшее программное обеспечение для виртуализации в 2020 году: Hyper-V, KVM, vSphere и XenServer (перевод)
- [] Выгорание сотрудников: основные принципы борьбы, если вы тимлид
- В Firefox 80 реализована настройка для перенаправления с HTTP на HTTPS
- [Тестирование IT-систем, Тестирование веб-сервисов] Тестирование: назад к основам + [Puppeteer][Mocha] Совершенствуйте код с помощью тестового покрытия (перевод)
- [Ненормальное программирование, Clojure, Функциональное программирование, Программирование] Функциональное программирование, знакомься — ООП (перевод)
- [Python] В каких случаях не нужно использовать списки в Python (перевод)
Теги для поиска: #_scala, #_funktsionalnoe_programmirovanie (Функциональное программирование), #_scala, #_tapir, #_akka, #_http, #_functional_programming, #_otus, #_blog_kompanii_otus._onlajnobrazovanie (
Блог компании OTUS. Онлайн-образование
), #_scala, #_funktsionalnoe_programmirovanie (
Функциональное программирование
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 20:22
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Перевод статьи подготовлен в преддверии старта курса «Scala-разработчик» Теа, Ральф и Джесси используют 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-разработчик» =========== Источник: habr.com =========== =========== Автор оригинала: Adam Warski ===========Похожие новости:
Блог компании OTUS. Онлайн-образование ), #_scala, #_funktsionalnoe_programmirovanie ( Функциональное программирование ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 20:22
Часовой пояс: UTC + 5