Привет Хабр! Следующих этап изучения нового языка это старый добрый todo list только не простой а с загрузкой и скачиванием картинок чтобы научится работе с базой данных и файловой системой. За подробностями добро пожаловать под кат.
Содержание
- Изучаю Scala: Часть 1 Игра змейка
- Изучаю Scala: Часть 2 Todo лист с возможностью загрузки картинок
Ссылки
Исходники
Образы 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 baseCOPY . /rootWORKDIR /rootRUN sbt universal:packageZipTarballRUN sbt testFROM openjdk:15-alpine as finalCOPY --from=base /root/target/scala-2.13/scala-todo-api.jar /rootWORKDIR /rootEXPOSE 8080ENTRYPOINT ["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]