Изучаю Scala: Часть 2 — Todo лист с возможностью загрузки картинок

от автора


Привет, Хабр! Следующих этап изучения нового языка это старый добрый todo list только не простой а с загрузкой и скачиванием картинок чтобы научится работе с базой данных и файловой системой. За подробностями добро пожаловать под кат.

Содержание

Ссылки

Исходники
Образы docker image

API

Описание апи в swagger и эндпойнты я сделал с помощью Tapir. Он позволяет своим DSL описать API которое мы хотим реализовать.

  def withStatus[A](f: IO[A]): IO[Either[(StatusCode, String), A]] =     f.attempt.map(x => x match {       case Right(value) => Right(value)       case Left(value) => Left(StatusCode.InternalServerError, value.getMessage)     })  val baseEndpoint = endpoint     .in("api" / "v1")     .errorOut(statusCode.and(stringBody))  private val baseImageEndpoint = baseEndpoint     .in("images")     .tag("Images")    private val download = baseImageEndpoint     .summary("Скачать картинку")     .description("Скачивает картику по ее идентификатору")     .get     .in(path[Long]("id"))     .out(header[Long](HeaderNames.ContentLength))     .out(streamBody[Stream[IO, Byte]](schemaFor[File], CodecFormat.OctetStream()))     .serverLogic(x => withStatus(imagesService.download(x))) 

на основе коллекции таких эндпойнтов создаются роуты, а на основе них документация Swagger

    endpoints = todosController.endpoints ::: imagesController.endpoints     routes = endpoints.toRoutes;     docs = endpoints.toOpenAPI("The Scala Todo List", "0.0.1")     yml: String = docs.toYaml     appRoutes = routes <+> new SwaggerHttp4s(yml, "swagger").routes[IO] 

Server

В качестве сервера Tapir поддерживает несколько бекендов. Я использовал http4s

    httpApp = Router(       "/" -> appRoutes     ).orNotFound     blazeServer <- BlazeServerBuilder[IO](serverEc)       .bindHttp(settings.host.port, settings.host.host)       .withHttpApp(httpApp)       .resource 

Работа с файлами и стримы

Для работы с файлами я использовал стримы из fs2

import fs2.{Stream, io}    def get(path: Path): Stream[IO, Byte] =     io.file.readAll[IO](path, blocker, 4096) 

Работа с базой данных

Для работы с БД я использовал doobie и он мне чертовски понравился потому что напомнил старый добрый Dapper ORM. Позволяет маппить DTO и выполнять SQL запросы.

  def add(image: Image): IO[Long] = sql"""          INSERT INTO images (hash, file_path)          VALUES (${image.hash}, ${image.filePath})""".update     .withUniqueGeneratedKeys[Long]("id")     .transact(xa) 

Сборка и упаковка в образ Docker

Я захотел собрать все в один единственный файл как например делает это Go или .NET Core с нужными настройками поэтому использовал sbt-native-packager и плагин к нему sbt-assembly. Собранный файл можно запустить с помощью команды

java -jar <имя файла> 

Потом сделал DockerFile для запуска этого образа в контейнере

FROM hseeberger/scala-sbt:11.0.2-oraclelinux7_1.3.12_2.13.3 AS base COPY . /root WORKDIR /root RUN sbt universal:packageZipTarball RUN sbt test  FROM openjdk:15-alpine as final COPY --from=base /root/target/scala-2.13/scala-todo-api.jar /root WORKDIR /root EXPOSE 8080 ENTRYPOINT ["java","-jar","scala-todo-api.jar"] 

Собранный образ автоматом отправляется в Registry гитлаба через его встроенный CI/CD

Настройки

Настройки сервера загружаю с помощью библиотеки PureConfig и потом так как я использую Docker дополняю их из переменных окружения. Файл application.conf:

db {   url = "jdbc:postgresql://localhost:5432/todos_db"   url = ${?TODO_API_DB_URL}   user = "postgres"   user = ${?TODO_API_DB_USER}   password = "postgres"   password = ${?TODO_API_DB_PASSWORD} } host {   port = 8080   port = ${?TODO_API_HOSTING_PORT}   host = "0.0.0.0"   host = ${?TODO_API_HOSTING_HOST} } 

val config = ConfigSource.default.load[AppSettings] 

ссылка на оригинал статьи https://habr.com/ru/post/508560/