Страничка курса: https://maxcom.github.io/scala-course-2022/
Как строить API поверх HTTP?
Можно инкапсулировать протокол поверх HTTP.
Но это сложно и не позволяет использовать полезные свойства протокола.
Используем, если протокол не "ложится" на логику HTTP.
REST - методика создания API поверх протокола HTTP.
Используем "фичи" протокола по максимуму.
"REST" как функционально программирование - его все любят, но никто точно не знает, что это такое.
Современный асинхронный HTTP сервер и клиент.
(основное использование - серверная сторона)
Никаких сервлетов, "веб-приложений" и т.п.
Построен на технологиях Akka.
Akka HTTP - веб-сервер на Akka, а не "для Akka".
Асинхронность - Future и Akka Streams.
Акторы используются только внутри реализации.
(будет отдельная лекция про Akka Streams)
Java-сервлеты научились быть асинхронными через 10 лет после появления первой версии.
И еще через 3 года появился
асинхронный ввод-вывод.
Начнем с Hello, world!
build.sbt
libraryDependencies +=
"com.typesafe.akka" %% "akka-http" % "10.2.9"
// версию Akka выбираем сами (>= 2.5)
libraryDependencies +=
"com.typesafe.akka" %% "akka-stream" % "2.6.19"
Импортируем scaladsl
import akka.http.scaladsl.model._
import akka.http.scaladsl.server.Directives._
(не путаем с javadsl, Java API)
// ActorSystem - ExecutionContext, Scheduler и др компоненты
implicit val system: ActorSystem = ActorSystem("my-system")
// обработчик запросов
val route: Route = ???
val bindingFuture: Future[Http.ServerBinding] =
Http().newServerAt("localhost", 8080).bind(route)
// не забываем про обработку bindingFuture:
// там могут быть ошибки
Hello, World!
val route: Route =
path("hello") { // путь "/hello"
get { // метод "GET"
complete(
HttpEntity( // по-умолчанию тип text/plain для String
ContentTypes.`text/html(UTF-8)`,
"Say hello to akka-http
"))
}
}
Обработчик запросов
type Route = RequestContext => Future[RouteResult]
// есть еще более низкоуровневый вариант
Стандартный роутинг (плюсы):
Стандартный роутинг (минусы):
Альтернативный роутинг:
Вернемся к REST:
URI должен указывать на объект на сервере
Например, профиль пользователя admin
/users/admin
Действия над пользователями:
Коды HTTP классифицируют ошибки
(подробности - в теле ответа)
Пример ошибок клиента
Ошибки сервера
type Route = RequestContext => Future[RouteResult]
RouteResult - ADT
Complete с ошибкой "финален", Rejected допускает альтернативу
Роутинг может быть такой:
path("users") {
post {
entity(as[String]) { login =>
createNewUser(login)
complete(StatusCodes.Created)
}
} ~
path(Segment) { login =>
get { ??? } ~
put { ??? } ~
patch { ??? } ~
delete { ??? }
}
}
Элементы роутинга - директивы (Directive).
Функции, оборачивающие другие функции роутинга.
Directive может:
abstract class Directive[L](implicit val ev: Tuple[L]) {
// tuple-apply, "низкий уровень"
def tapply(f: L => Route): Route
... // функции модификации и композиции
}
tapply извлекает аргументы из tuple
Пример:
path("hello") { // Directive0
get { // Directive0
complete("Say hello to akka-http")
}
} ~
// Directive1 требует путь "order/$id" и извлекает id
path("order" / IntNumber) { id =>
complete(s"Order $id")
}
Композиция:
path("order" / IntNumber) { id =>
(get | put) {
extractMethod { m =>
complete(s"Received ${m.name} request for order $id")
}
}
}
// соберем custom директиву
val orderGetOrPutWithMethod =
path("order" / IntNumber) & (get | put) & extractMethod
val route =
orderGetOrPutWithMethod { (id, method) =>
complete(s"Received ${method.name} request for order $id")
}
Работа с Future
val future: Future[String] = Future.successful("value")
val route =
path("success") {
onSuccess(future) { v => // Directive[Tuple1[String]]
complete(s"Future was completed with $v.")
}
}
def complete(m: => ToResponseMarshallable): StandardRoute
// implicit преобразование
typeclass напрямую не используется
(implicit параметры ломают "магию" routing DSL)
type ToResponseMarshaller[T] = Marshaller[T, HttpResponse]
sealed abstract class Marshaller[-A, +B] {
def apply(value: A)(implicit ec: ExecutionContext):
Future[List[Marshalling[B]]]
...
}
список вариантов для content negotiation
JSON: akka-http-json
build.sbt
libraryDependencies +=
"de.heikoseeberger" %% "akka-http-play-json" % "1.39.2"
выходят очень часто - одна версия на все json библиотеки
import de.heikoseeberger.akkahttpplayjson.PlayJsonSupport._
...
complete(Json.obj("value" -> "Hello, world!"))
...
можно передать не JsObject, а всё, для чего есть Writes
case class Data(value: String)
object Data {
implicit val format: OFormat[Data] = Json.format[Data]
}
Format в play-json - комбинация Reads и Writes
OFormat - комбинация Reads и OWrites
get {
complete(Data("Hello, world"))
} ~ post {
// entity - Directive1
// as[Data] ищет implicit FromRequestUnmarshaller[T]
entity(as[Data]) { data =>
complete(s"Got ${data.value}")
}
}
Напоминаю: