{"id":306064,"date":"2020-06-28T21:01:02","date_gmt":"2020-06-28T21:01:02","guid":{"rendered":"http:\/\/savepearlharbor.com\/?p=306064"},"modified":"-0001-11-30T00:00:00","modified_gmt":"-0001-11-29T21:00:00","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=306064","title":{"rendered":"\u0418\u0437\u0443\u0447\u0430\u044e Scala: \u0427\u0430\u0441\u0442\u044c 2 \u2014 Todo \u043b\u0438\u0441\u0442 \u0441 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c\u044e \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 \u043a\u0430\u0440\u0442\u0438\u043d\u043e\u043a"},"content":{"rendered":"\n<div class=\"post__text post__text-html post__text_v1\" id=\"post-content-body\" data-io-article-url=\"https:\/\/habr.com\/ru\/post\/508560\/\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/webt\/vl\/jk\/yx\/vljkyxqqcfc48zh07oj2twr3n4c.png\"><br \/>  \u041f\u0440\u0438\u0432\u0435\u0442, \u0425\u0430\u0431\u0440! \u0421\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0445 \u044d\u0442\u0430\u043f \u0438\u0437\u0443\u0447\u0435\u043d\u0438\u044f \u043d\u043e\u0432\u043e\u0433\u043e \u044f\u0437\u044b\u043a\u0430 \u044d\u0442\u043e \u0441\u0442\u0430\u0440\u044b\u0439 \u0434\u043e\u0431\u0440\u044b\u0439 todo list \u0442\u043e\u043b\u044c\u043a\u043e \u043d\u0435 \u043f\u0440\u043e\u0441\u0442\u043e\u0439 \u0430 \u0441 \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u043e\u0439 \u0438 \u0441\u043a\u0430\u0447\u0438\u0432\u0430\u043d\u0438\u0435\u043c \u043a\u0430\u0440\u0442\u0438\u043d\u043e\u043a \u0447\u0442\u043e\u0431\u044b \u043d\u0430\u0443\u0447\u0438\u0442\u0441\u044f \u0440\u0430\u0431\u043e\u0442\u0435 \u0441 \u0431\u0430\u0437\u043e\u0439 \u0434\u0430\u043d\u043d\u044b\u0445 \u0438 \u0444\u0430\u0439\u043b\u043e\u0432\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u043e\u0439. \u0417\u0430 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0441\u0442\u044f\u043c\u0438 \u0434\u043e\u0431\u0440\u043e \u043f\u043e\u0436\u0430\u043b\u043e\u0432\u0430\u0442\u044c \u043f\u043e\u0434 \u043a\u0430\u0442. <br \/>  <a name=\"habracut\"><\/a>  <\/p>\n<h2>\u0421\u043e\u0434\u0435\u0440\u0436\u0430\u043d\u0438\u0435<\/h2>\n<p>  <\/p>\n<ul>\n<li><a href=\"https:\/\/habr.com\/ru\/post\/503560\/\">\u0418\u0437\u0443\u0447\u0430\u044e Scala: \u0427\u0430\u0441\u0442\u044c 1 \u2014 \u0418\u0433\u0440\u0430 \u0437\u043c\u0435\u0439\u043a\u0430<\/a><\/li>\n<li>\u0418\u0437\u0443\u0447\u0430\u044e Scala: \u0427\u0430\u0441\u0442\u044c 2 \u2014 Todo \u043b\u0438\u0441\u0442 \u0441 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c\u044e \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 \u043a\u0430\u0440\u0442\u0438\u043d\u043e\u043a<\/li>\n<\/ul>\n<p>  <\/p>\n<h2>\u0421\u0441\u044b\u043b\u043a\u0438<\/h2>\n<p>  <a href=\"https:\/\/gitlab.com\/VictorWinbringer\/scalatodoapi\" rel=\"nofollow\">\u0418\u0441\u0445\u043e\u0434\u043d\u0438\u043a\u0438<\/a><br \/>  <a href=\"https:\/\/gitlab.com\/VictorWinbringer\/scalatodoapi\/container_registry\/\" rel=\"nofollow\">\u041e\u0431\u0440\u0430\u0437\u044b docker image<\/a><\/p>\n<h2>API<\/h2>\n<p>  \u041e\u043f\u0438\u0441\u0430\u043d\u0438\u0435 \u0430\u043f\u0438 \u0432 swagger \u0438 \u044d\u043d\u0434\u043f\u043e\u0439\u043d\u0442\u044b \u044f \u0441\u0434\u0435\u043b\u0430\u043b \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e <a href=\"https:\/\/github.com\/softwaremill\/tapir\" rel=\"nofollow\">Tapir<\/a>. \u041e\u043d \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0441\u0432\u043e\u0438\u043c DSL \u043e\u043f\u0438\u0441\u0430\u0442\u044c API \u043a\u043e\u0442\u043e\u0440\u043e\u0435 \u043c\u044b \u0445\u043e\u0442\u0438\u043c \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u0442\u044c.<\/p>\n<pre><code class=\"scala\">  def withStatus[A](f: IO[A]): IO[Either[(StatusCode, String), A]] =     f.attempt.map(x =&gt; x match {       case Right(value) =&gt; Right(value)       case Left(value) =&gt; Left(StatusCode.InternalServerError, value.getMessage)     })  val baseEndpoint = endpoint     .in(&quot;api&quot; \/ &quot;v1&quot;)     .errorOut(statusCode.and(stringBody))  private val baseImageEndpoint = baseEndpoint     .in(&quot;images&quot;)     .tag(&quot;Images&quot;)    private val download = baseImageEndpoint     .summary(&quot;\u0421\u043a\u0430\u0447\u0430\u0442\u044c \u043a\u0430\u0440\u0442\u0438\u043d\u043a\u0443&quot;)     .description(&quot;\u0421\u043a\u0430\u0447\u0438\u0432\u0430\u0435\u0442 \u043a\u0430\u0440\u0442\u0438\u043a\u0443 \u043f\u043e \u0435\u0435 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u0443&quot;)     .get     .in(path[Long](&quot;id&quot;))     .out(header[Long](HeaderNames.ContentLength))     .out(streamBody[Stream[IO, Byte]](schemaFor[File], CodecFormat.OctetStream()))     .serverLogic(x =&gt; withStatus(imagesService.download(x))) <\/code><\/pre>\n<p>  \u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u0435 \u043a\u043e\u043b\u043b\u0435\u043a\u0446\u0438\u0438 \u0442\u0430\u043a\u0438\u0445 \u044d\u043d\u0434\u043f\u043e\u0439\u043d\u0442\u043e\u0432 \u0441\u043e\u0437\u0434\u0430\u044e\u0442\u0441\u044f \u0440\u043e\u0443\u0442\u044b, \u0430 \u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u0435 \u043d\u0438\u0445 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f Swagger <\/p>\n<pre><code class=\"scala\">    endpoints = todosController.endpoints ::: imagesController.endpoints     routes = endpoints.toRoutes;     docs = endpoints.toOpenAPI(&quot;The Scala Todo List&quot;, &quot;0.0.1&quot;)     yml: String = docs.toYaml     appRoutes = routes &lt;+&gt; new SwaggerHttp4s(yml, &quot;swagger&quot;).routes[IO] <\/code><\/pre>\n<p>  <\/p>\n<h2>Server<\/h2>\n<p>  \u0412 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 Tapir \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0431\u0435\u043a\u0435\u043d\u0434\u043e\u0432. \u042f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043b <a href=\"https:\/\/github.com\/http4s\/http4s\" rel=\"nofollow\">http4s<\/a><\/p>\n<pre><code class=\"scala\">    httpApp = Router(       &quot;\/&quot; -&gt; appRoutes     ).orNotFound     blazeServer &lt;- BlazeServerBuilder[IO](serverEc)       .bindHttp(settings.host.port, settings.host.host)       .withHttpApp(httpApp)       .resource <\/code><\/pre>\n<p>  <\/p>\n<h2>\u0420\u0430\u0431\u043e\u0442\u0430 \u0441 \u0444\u0430\u0439\u043b\u0430\u043c\u0438 \u0438 \u0441\u0442\u0440\u0438\u043c\u044b<\/h2>\n<p>  \u0414\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0441 \u0444\u0430\u0439\u043b\u0430\u043c\u0438 \u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043b \u0441\u0442\u0440\u0438\u043c\u044b \u0438\u0437 <a href=\"https:\/\/github.com\/functional-streams-for-scala\/fs2\" rel=\"nofollow\">fs2<\/a><\/p>\n<pre><code class=\"scala\">import fs2.{Stream, io}    def get(path: Path): Stream[IO, Byte] =     io.file.readAll[IO](path, blocker, 4096) <\/code><\/pre>\n<p>  <\/p>\n<h2>\u0420\u0430\u0431\u043e\u0442\u0430 \u0441 \u0431\u0430\u0437\u043e\u0439 \u0434\u0430\u043d\u043d\u044b\u0445<\/h2>\n<p>  \u0414\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0441 \u0411\u0414 \u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043b <a href=\"https:\/\/github.com\/tpolecat\/doobie\" rel=\"nofollow\">doobie<\/a> \u0438 \u043e\u043d \u043c\u043d\u0435 \u0447\u0435\u0440\u0442\u043e\u0432\u0441\u043a\u0438 \u043f\u043e\u043d\u0440\u0430\u0432\u0438\u043b\u0441\u044f \u043f\u043e\u0442\u043e\u043c\u0443 \u0447\u0442\u043e \u043d\u0430\u043f\u043e\u043c\u043d\u0438\u043b \u0441\u0442\u0430\u0440\u044b\u0439 \u0434\u043e\u0431\u0440\u044b\u0439 Dapper ORM. \u041f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u043c\u0430\u043f\u043f\u0438\u0442\u044c DTO \u0438 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0442\u044c SQL \u0437\u0430\u043f\u0440\u043e\u0441\u044b.<\/p>\n<pre><code class=\"scala\">  def add(image: Image): IO[Long] = sql&quot;&quot;&quot;          INSERT INTO images (hash, file_path)          VALUES (${image.hash}, ${image.filePath})&quot;&quot;&quot;.update     .withUniqueGeneratedKeys[Long](&quot;id&quot;)     .transact(xa) <\/code><\/pre>\n<p>  <\/p>\n<h2>\u0421\u0431\u043e\u0440\u043a\u0430 \u0438 \u0443\u043f\u0430\u043a\u043e\u0432\u043a\u0430 \u0432 \u043e\u0431\u0440\u0430\u0437 Docker<\/h2>\n<p>  \u042f \u0437\u0430\u0445\u043e\u0442\u0435\u043b \u0441\u043e\u0431\u0440\u0430\u0442\u044c \u0432\u0441\u0435 \u0432 \u043e\u0434\u0438\u043d \u0435\u0434\u0438\u043d\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0439 \u0444\u0430\u0439\u043b \u043a\u0430\u043a \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440 \u0434\u0435\u043b\u0430\u0435\u0442 \u044d\u0442\u043e Go \u0438\u043b\u0438 .NET Core \u0441 \u043d\u0443\u0436\u043d\u044b\u043c\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u043c\u0438 \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043b <a href=\"https:\/\/github.com\/sbt\/sbt-native-packager\" rel=\"nofollow\">sbt-native-packager<\/a> \u0438 \u043f\u043b\u0430\u0433\u0438\u043d \u043a \u043d\u0435\u043c\u0443 <a href=\"https:\/\/github.com\/sbt\/sbt-assembly\" rel=\"nofollow\">sbt-assembly<\/a>. \u0421\u043e\u0431\u0440\u0430\u043d\u043d\u044b\u0439 \u0444\u0430\u0439\u043b \u043c\u043e\u0436\u043d\u043e \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043a\u043e\u043c\u0430\u043d\u0434\u044b <\/p>\n<pre><code class=\"bash\">java -jar &lt;\u0438\u043c\u044f \u0444\u0430\u0439\u043b\u0430&gt; <\/code><\/pre>\n<p>  \u041f\u043e\u0442\u043e\u043c \u0441\u0434\u0435\u043b\u0430\u043b DockerFile \u0434\u043b\u044f \u0437\u0430\u043f\u0443\u0441\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u043e\u0431\u0440\u0430\u0437\u0430 \u0432 \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0435 <\/p>\n<pre><code class=\"powershell\">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 [&quot;java&quot;,&quot;-jar&quot;,&quot;scala-todo-api.jar&quot;] <\/code><\/pre>\n<p>  \u0421\u043e\u0431\u0440\u0430\u043d\u043d\u044b\u0439 \u043e\u0431\u0440\u0430\u0437 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u043e\u043c \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0432 <a href=\"https:\/\/gitlab.com\/VictorWinbringer\/scalatodoapi\/container_registry\" rel=\"nofollow\">Registry<\/a> \u0433\u0438\u0442\u043b\u0430\u0431\u0430 \u0447\u0435\u0440\u0435\u0437 \u0435\u0433\u043e \u0432\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u044b\u0439 CI\/CD<\/p>\n<h2>\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438<\/h2>\n<p>  \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u0437\u0430\u0433\u0440\u0443\u0436\u0430\u044e \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0438 <a href=\"https:\/\/github.com\/pureconfig\/pureconfig\" rel=\"nofollow\">PureConfig<\/a> \u0438 \u043f\u043e\u0442\u043e\u043c \u0442\u0430\u043a \u043a\u0430\u043a \u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e Docker \u0434\u043e\u043f\u043e\u043b\u043d\u044f\u044e \u0438\u0445 \u0438\u0437 \u043f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0445 \u043e\u043a\u0440\u0443\u0436\u0435\u043d\u0438\u044f. \u0424\u0430\u0439\u043b application.conf:<\/p>\n<pre><code class=\"bash\">db {   url = &quot;jdbc:postgresql:\/\/localhost:5432\/todos_db&quot;   url = ${?TODO_API_DB_URL}   user = &quot;postgres&quot;   user = ${?TODO_API_DB_USER}   password = &quot;postgres&quot;   password = ${?TODO_API_DB_PASSWORD} } host {   port = 8080   port = ${?TODO_API_HOSTING_PORT}   host = &quot;0.0.0.0&quot;   host = ${?TODO_API_HOSTING_HOST} } <\/code><\/pre>\n<p>  <\/p>\n<pre><code class=\"scala\">val config = ConfigSource.default.load[AppSettings] <\/code><\/pre>\n<\/div>\n<p> \u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/post\/508560\/\"> https:\/\/habr.com\/ru\/post\/508560\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"\n<div class=\"post__text post__text-html post__text_v1\" id=\"post-content-body\" data-io-article-url=\"https:\/\/habr.com\/ru\/post\/508560\/\"><img decoding=\"async\" src=\"https:\/\/habrastorage.org\/webt\/vl\/jk\/yx\/vljkyxqqcfc48zh07oj2twr3n4c.png\"><br \/>  \u041f\u0440\u0438\u0432\u0435\u0442, \u0425\u0430\u0431\u0440! \u0421\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0445 \u044d\u0442\u0430\u043f \u0438\u0437\u0443\u0447\u0435\u043d\u0438\u044f \u043d\u043e\u0432\u043e\u0433\u043e \u044f\u0437\u044b\u043a\u0430 \u044d\u0442\u043e \u0441\u0442\u0430\u0440\u044b\u0439 \u0434\u043e\u0431\u0440\u044b\u0439 todo list \u0442\u043e\u043b\u044c\u043a\u043e \u043d\u0435 \u043f\u0440\u043e\u0441\u0442\u043e\u0439 \u0430 \u0441 \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u043e\u0439 \u0438 \u0441\u043a\u0430\u0447\u0438\u0432\u0430\u043d\u0438\u0435\u043c \u043a\u0430\u0440\u0442\u0438\u043d\u043e\u043a \u0447\u0442\u043e\u0431\u044b \u043d\u0430\u0443\u0447\u0438\u0442\u0441\u044f \u0440\u0430\u0431\u043e\u0442\u0435 \u0441 \u0431\u0430\u0437\u043e\u0439 \u0434\u0430\u043d\u043d\u044b\u0445 \u0438 \u0444\u0430\u0439\u043b\u043e\u0432\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u043e\u0439. \u0417\u0430 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0441\u0442\u044f\u043c\u0438 \u0434\u043e\u0431\u0440\u043e \u043f\u043e\u0436\u0430\u043b\u043e\u0432\u0430\u0442\u044c \u043f\u043e\u0434 \u043a\u0430\u0442.   <\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-306064","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/306064","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=306064"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/306064\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=306064"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=306064"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=306064"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}