From ecad081d93d25bf323bd0123d4d44fc5b2577298 Mon Sep 17 00:00:00 2001 From: Salar Rahmanian Date: Sat, 27 Apr 2024 00:38:34 -0700 Subject: [PATCH] upgrade to scala3 and migrate to fs2.io for file handling --- .github/workflows/ci.yml | 113 +++++++++--------- .scalafmt.conf | 2 +- build.sbt | 35 +++--- .../com/softinio/scalanews/Bloggers.scala | 39 +++--- .../com/softinio/scalanews/ConfigLoader.scala | 4 +- .../com/softinio/scalanews/FileHandler.scala | 67 +++++++---- .../com/softinio/scalanews/HttpClient.scala | 2 +- .../scala/com/softinio/scalanews/Main.scala | 38 +++--- .../scala/com/softinio/scalanews/Rome.scala | 2 +- .../softinio/scalanews/algebra/Article.scala | 2 +- .../scalanews/algebra/Configuration.scala | 12 +- .../softinio/scalanews/BloggersSuite.scala | 2 +- .../scalanews/ConfigLoaderSuite.scala | 2 +- .../softinio/scalanews/FileHandlerSuite.scala | 53 ++++---- .../softinio/scalanews/HttpClientSuite.scala | 5 +- .../com/softinio/scalanews/RomeSuite.scala | 2 +- flake.lock | 24 ++-- project/build.properties | 2 +- project/plugins.sbt | 4 +- 19 files changed, 226 insertions(+), 184 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c1ff66d..abc3b3e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,123 +15,126 @@ on: tags: [v*] env: - PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} - SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} - SONATYPE_CREDENTIAL_HOST: ${{ secrets.SONATYPE_CREDENTIAL_HOST }} - SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} - PGP_SECRET: ${{ secrets.PGP_SECRET }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + +concurrency: + group: ${{ github.workflow }} @ ${{ github.ref }} + cancel-in-progress: true + jobs: build: name: Build and Test strategy: matrix: os: [ubuntu-latest] - scala: [2.13.12] + scala: [3] java: [corretto@21] project: [rootJVM] runs-on: ${{ matrix.os }} + timeout-minutes: 60 steps: - name: Checkout current branch (full) - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Download Java (corretto@21) - id: download-java-corretto-21 - if: matrix.java == 'corretto@21' - uses: typelevel/download-java@v1 - with: - distribution: corretto - java-version: 21 - - name: Setup Java (corretto@21) + id: setup-java-corretto-21 if: matrix.java == 'corretto@21' - uses: actions/setup-java@v2 + uses: actions/setup-java@v4 with: - distribution: jdkfile + distribution: corretto java-version: 21 - jdkFile: ${{ steps.download-java-corretto-21.outputs.jdkFile }} + cache: sbt - - name: Cache sbt - uses: actions/cache@v2 - with: - path: | - ~/.sbt - ~/.ivy2/cache - ~/.coursier/cache/v1 - ~/.cache/coursier/v1 - ~/AppData/Local/Coursier/Cache/v1 - ~/Library/Caches/Coursier/v1 - key: ${{ runner.os }}-sbt-cache-v2-${{ hashFiles('**/*.sbt') }}-${{ hashFiles('project/build.properties') }} + - name: sbt update + if: matrix.java == 'corretto@21' && steps.setup-java-corretto-21.outputs.cache-hit == 'false' + run: sbt +update - name: Check that workflows are up to date run: sbt githubWorkflowCheck - name: Check headers and formatting - if: matrix.java == 'corretto@21' + if: matrix.java == 'corretto@21' && matrix.os == 'ubuntu-latest' run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' headerCheckAll scalafmtCheckAll 'project /' scalafmtSbtCheck - name: Test run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' test - name: Check binary compatibility - if: matrix.java == 'corretto@21' + if: matrix.java == 'corretto@21' && matrix.os == 'ubuntu-latest' run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' mimaReportBinaryIssues - name: Generate API documentation - if: matrix.java == 'corretto@21' + if: matrix.java == 'corretto@21' && matrix.os == 'ubuntu-latest' run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' doc - site: - name: Generate Site + dependency-submission: + name: Submit Dependencies + if: github.event_name != 'pull_request' strategy: matrix: os: [ubuntu-latest] - scala: [2.13.12] java: [corretto@21] runs-on: ${{ matrix.os }} steps: - name: Checkout current branch (full) - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Download Java (corretto@21) - id: download-java-corretto-21 + - name: Setup Java (corretto@21) + id: setup-java-corretto-21 if: matrix.java == 'corretto@21' - uses: typelevel/download-java@v1 + uses: actions/setup-java@v4 with: distribution: corretto java-version: 21 + cache: sbt + + - name: sbt update + if: matrix.java == 'corretto@21' && steps.setup-java-corretto-21.outputs.cache-hit == 'false' + run: sbt +update + + - name: Submit Dependencies + uses: scalacenter/sbt-dependency-submission@v2 + with: + modules-ignore: rootjs_3 docs_3 rootjvm_3 rootnative_3 + configs-ignore: test scala-tool scala-doc-tool test-internal + + site: + name: Generate Site + strategy: + matrix: + os: [ubuntu-latest] + java: [corretto@21] + runs-on: ${{ matrix.os }} + steps: + - name: Checkout current branch (full) + uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Setup Java (corretto@21) + id: setup-java-corretto-21 if: matrix.java == 'corretto@21' - uses: actions/setup-java@v2 + uses: actions/setup-java@v4 with: - distribution: jdkfile + distribution: corretto java-version: 21 - jdkFile: ${{ steps.download-java-corretto-21.outputs.jdkFile }} + cache: sbt - - name: Cache sbt - uses: actions/cache@v2 - with: - path: | - ~/.sbt - ~/.ivy2/cache - ~/.coursier/cache/v1 - ~/.cache/coursier/v1 - ~/AppData/Local/Coursier/Cache/v1 - ~/Library/Caches/Coursier/v1 - key: ${{ runner.os }}-sbt-cache-v2-${{ hashFiles('**/*.sbt') }}-${{ hashFiles('project/build.properties') }} + - name: sbt update + if: matrix.java == 'corretto@21' && steps.setup-java-corretto-21.outputs.cache-hit == 'false' + run: sbt +update - name: Generate site - run: sbt '++ ${{ matrix.scala }}' docs/tlSite + run: sbt docs/tlSite - name: Publish site if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' - uses: peaceiris/actions-gh-pages@v3.8.0 + uses: peaceiris/actions-gh-pages@v3.9.3 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: site/target/docs/site diff --git a/.scalafmt.conf b/.scalafmt.conf index d21ae24..cd02d3a 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,2 +1,2 @@ version = 3.4.3 -runner.dialect = scala213 +runner.dialect = scala3 diff --git a/build.sbt b/build.sbt index 95477d4..1253981 100644 --- a/build.sbt +++ b/build.sbt @@ -11,7 +11,7 @@ ThisBuild / tlBaseVersion := "0.1" // your current series x.y ThisBuild / organization := "com.softinio" ThisBuild / organizationName := "Salar Rahmanian" -ThisBuild / startYear := Some(2023) +ThisBuild / startYear := Some(2024) ThisBuild / licenses := Seq(License.Apache2) ThisBuild / developers := List( // your GitHub handle and name @@ -25,11 +25,9 @@ ThisBuild / tlSitePublishBranch := Some("main") ThisBuild / githubWorkflowJavaVersions := Seq(JavaSpec.corretto("21")) -val Scala213 = "2.13.12" -ThisBuild / crossScalaVersions := Seq(Scala213) -ThisBuild / scalaVersion := Scala213 // the default Scala - -addCompilerPlugin("com.olegpy" %% "better-monadic-for" % "0.3.0") +val Scala3 = "3.3.3" +ThisBuild / crossScalaVersions := Seq(Scala3) +ThisBuild / scalaVersion := Scala3 // the default Scala lazy val root = tlCrossRootProject.aggregate(core) @@ -40,27 +38,28 @@ lazy val core = crossProject(JVMPlatform) name := "scalanews", libraryDependencies ++= Seq( "org.typelevel" %% "cats-core" % "2.10.0", - "org.typelevel" %% "cats-effect" % "3.5.2", - "io.github.akiomik" %% "cats-nio-file" % "1.10.0", + "org.typelevel" %% "cats-effect" % "3.5.4", "com.monovore" %% "decline-effect" % "2.4.1", - "com.github.pureconfig" %% "pureconfig" % "0.17.4", - "com.github.pureconfig" %% "pureconfig-cats-effect" % "0.17.4", - "org.http4s" %% "http4s-ember-client" % "0.23.24", - "org.http4s" %% "http4s-dsl" % "0.23.24", + "com.github.pureconfig" %% "pureconfig-core" % "0.17.6", + "com.github.pureconfig" %% "pureconfig-cats-effect" % "0.17.6", + "org.http4s" %% "http4s-ember-client" % "0.23.26", + "org.http4s" %% "http4s-dsl" % "0.23.26", + "co.fs2" %% "fs2-core" % "3.10.2", + "co.fs2" %% "fs2-io" % "3.10.2", "com.rometools" % "rome" % "2.1.0", - "org.scalameta" %% "munit" % "0.7.29" % Test, - "org.typelevel" %% "munit-cats-effect-3" % "1.0.7" % Test + "org.scalameta" %% "munit" % "1.0.0-RC1" % Test, + "org.typelevel" %% "munit-cats-effect" % "2.0.0-M5" % Test ), Compile / mainClass := Some("com.softinio.scalanews.Main"), - nativeImageVersion := "21.0.1", + nativeImageVersion := "21.0.2", nativeImageJvm := "graalvm-java21", nativeImageOptions += "--no-fallback", nativeImageOptions += "--enable-url-protocols=http", nativeImageOptions += "--enable-url-protocols=https", nativeImageOutput := file(".") / "scalanews", - nativeImageReady := { () => println("SBT Finished creating image.") }, - resolvers += - "Sonatype OSS Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots" + nativeImageReady := { () => println("SBT Finished creating image.") } + // resolvers += + // "Sonatype OSS Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots" ) .enablePlugins(NativeImagePlugin) diff --git a/core/src/main/scala/com/softinio/scalanews/Bloggers.scala b/core/src/main/scala/com/softinio/scalanews/Bloggers.scala index 48d3414..49f3446 100644 --- a/core/src/main/scala/com/softinio/scalanews/Bloggers.scala +++ b/core/src/main/scala/com/softinio/scalanews/Bloggers.scala @@ -1,5 +1,5 @@ /* - * Copyright 2023 Salar Rahmanian + * Copyright 2024 Salar Rahmanian * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,10 @@ package com.softinio.scalanews -import java.nio.file.Paths -import java.nio.file.StandardOpenOption import java.util.Date import cats.effect._ -import cats.nio.file.Files +import fs2.io.file._ +import fs2.Stream import com.rometools.rome.feed.synd.SyndEntry import scala.jdk.CollectionConverters._ @@ -29,9 +28,9 @@ import com.softinio.scalanews.algebra.Blog object Bloggers { private val nextMarkdownFilePath = - Paths.get("next/next.md") + Path("next/next.md") private val directoryMarkdownFilePath = - Paths.get("docs/Resources/Blog_Directory.md") + Path("docs/Resources/Blog_Directory.md") private val blogsToSkipByUrl = List( "petr-zapletal.medium.com", "sudarshankasar.medium.com" @@ -39,7 +38,7 @@ object Bloggers { def generateDirectory(bloggerList: List[Blog]): IO[String] = { IO.blocking { val header = """ - |# Blog Directory + |# Blog Directory |A Directory of bloggers producing Scala related content with links to their rss feed when available. @@ -177,11 +176,14 @@ object Bloggers { exists <- Files[IO].exists(directoryMarkdownFilePath) _ <- if (exists) Files[IO].delete(directoryMarkdownFilePath) else IO.unit directory <- generateDirectory(bloggerList) - _ <- Files[IO].write( - directoryMarkdownFilePath, - directory.getBytes(), - StandardOpenOption.CREATE_NEW - ) + _ <- fs2.Stream + .emits(List(directory)) + .through(fs2.text.utf8.encode) + .through( + Files[IO].writeAll(directoryMarkdownFilePath, Flags(Flag.CreateNew)) + ) + .compile + .drain } yield ExitCode.Success } @@ -194,11 +196,14 @@ object Bloggers { _ <- if (exists) Files[IO].delete(nextMarkdownFilePath) else IO.unit articleList <- createBlogList(startDate, endDate) news <- generateNews(articleList) - _ <- Files[IO].write( - nextMarkdownFilePath, - news.getBytes(), - StandardOpenOption.CREATE_NEW - ) + _ <- fs2.Stream + .emits(List(news)) + .through(fs2.text.utf8.encode) + .through( + Files[IO].writeAll(directoryMarkdownFilePath, Flags(Flag.CreateNew)) + ) + .compile + .drain } yield ExitCode.Success } } diff --git a/core/src/main/scala/com/softinio/scalanews/ConfigLoader.scala b/core/src/main/scala/com/softinio/scalanews/ConfigLoader.scala index 344bddb..e767cee 100644 --- a/core/src/main/scala/com/softinio/scalanews/ConfigLoader.scala +++ b/core/src/main/scala/com/softinio/scalanews/ConfigLoader.scala @@ -1,5 +1,5 @@ /* - * Copyright 2023 Salar Rahmanian + * Copyright 2024 Salar Rahmanian * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,10 +17,8 @@ package com.softinio.scalanews import pureconfig._ -import pureconfig.generic.auto._ import pureconfig.module.catseffect.syntax._ import cats.effect.IO - import com.softinio.scalanews.algebra.Configuration object ConfigLoader { diff --git a/core/src/main/scala/com/softinio/scalanews/FileHandler.scala b/core/src/main/scala/com/softinio/scalanews/FileHandler.scala index 3e23565..fceee92 100644 --- a/core/src/main/scala/com/softinio/scalanews/FileHandler.scala +++ b/core/src/main/scala/com/softinio/scalanews/FileHandler.scala @@ -1,5 +1,5 @@ /* - * Copyright 2023 Salar Rahmanian + * Copyright 2024 Salar Rahmanian * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,36 +16,51 @@ package com.softinio.scalanews -import java.nio.file.{Files => JFiles} -import java.nio.file.Paths -import java.nio.file.Path -import java.nio.file.StandardCopyOption import java.time.format.DateTimeFormatter.BASIC_ISO_DATE import java.time.format.DateTimeFormatter import java.time.format.FormatStyle import java.time.LocalDate +import fs2.text import cats.effect._ -import cats.nio.file.Files +import fs2.io.file._ object FileHandler { - val nextFilePath = Paths.get("next/next.md") - val templateFilePath = Paths.get("next/template.md") - val indexFilePath = Paths.get("docs/index.md") + private val nextFilePath = Path("next/next.md") + private val templateFilePath = Path("next/template.md") + private val indexFilePath = Path("docs/index.md") def updateFileHeader( sourceFile: Path, headerDate: LocalDate - ): IO[Either[Throwable, Path]] = - IO.blocking { - val HEADER_TEXT = "# Scala News" - val headerDateString = - headerDate.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG)) - val content = JFiles.readString(sourceFile) - val updatedContent = - content.replace(HEADER_TEXT, s"$HEADER_TEXT - $headerDateString") - JFiles.writeString(sourceFile, updatedContent) - }.attempt + ): IO[Either[Throwable, Path]] = { + val HEADER_TEXT = "# Scala News" + val headerDateString = + headerDate.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG)) + val updatedHeader = s"$HEADER_TEXT - $headerDateString" + + Files[IO].tempFile().use { tempFile => + val usingTempFile = Path.fromNioPath(tempFile) + val updatedContent = for { + _ <- Files[IO] + .readAll(sourceFile) + .through(text.utf8.decode) + .through(text.lines) + .map(line => + if (line.startsWith(HEADER_TEXT)) updatedHeader else line + ) + .intersperse("\n") + .through(text.utf8.encode) + .through(Files[IO].writeAll(usingTempFile)) + .compile + .drain + _ <- Files[IO] + .move(usingTempFile, sourceFile, CopyFlags(CopyFlag.ReplaceExisting)) + } yield sourceFile + + updatedContent.attempt + } + } def getArchiveDate(archiveDate: String): IO[Either[Throwable, LocalDate]] = IO.blocking { @@ -70,24 +85,26 @@ object FileHandler { Files[IO].copy( templateFilePath, nextFilePath, - StandardCopyOption.REPLACE_EXISTING + CopyFlags(CopyFlag.ReplaceExisting) ) else if (!exists) Files[IO].copy(templateFilePath, nextFilePath) else IO.unit } yield (ExitCode.Success) - def createArchiveFolderPath(archiveFolder: Option[String]): IO[String] = { + private def createArchiveFolderPath( + archiveFolder: Option[String] + ): IO[String] = { IO.blocking { val folderPath = archiveFolder match { case Some(folder) => s"docs/Archive/${folder}/" case None => s"docs/Archive/" } - JFiles.createDirectories(Paths.get(folderPath)) + Files[IO].createDirectories(Path(folderPath)) folderPath } } - def createArchiveFileName(archiveDate: String): IO[String] = + private def createArchiveFileName(archiveDate: String): IO[String] = for { aDate <- getArchiveDate(archiveDate) fileName <- aDate match { @@ -96,14 +113,14 @@ object FileHandler { } } yield (fileName) - def getArchivePath( + private def getArchivePath( archiveDate: String, archiveFolder: Option[String] ): IO[Path] = for { fileName <- createArchiveFileName(archiveDate) folderPath <- createArchiveFolderPath(archiveFolder) - } yield (Paths.get(s"${folderPath}${fileName}")) + } yield (Path(s"${folderPath}${fileName}")) def publish( publishDate: Option[String], diff --git a/core/src/main/scala/com/softinio/scalanews/HttpClient.scala b/core/src/main/scala/com/softinio/scalanews/HttpClient.scala index 4cdab04..1b719ac 100644 --- a/core/src/main/scala/com/softinio/scalanews/HttpClient.scala +++ b/core/src/main/scala/com/softinio/scalanews/HttpClient.scala @@ -1,5 +1,5 @@ /* - * Copyright 2023 Salar Rahmanian + * Copyright 2024 Salar Rahmanian * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/core/src/main/scala/com/softinio/scalanews/Main.scala b/core/src/main/scala/com/softinio/scalanews/Main.scala index 6be70c4..bc8fd51 100644 --- a/core/src/main/scala/com/softinio/scalanews/Main.scala +++ b/core/src/main/scala/com/softinio/scalanews/Main.scala @@ -1,5 +1,5 @@ /* - * Copyright 2023 Salar Rahmanian + * Copyright 2024 Salar Rahmanian * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,35 +31,35 @@ object Main version = "0.1" ) { - case class Publish( + private case class Publish( publishDate: Option[String], archiveDate: String, archiveFolder: Option[String] ) - case class Create(overwrite: Boolean) + private case class Create(overwrite: Boolean) - case class Blogger(directory: Boolean) + private case class Blogger(directory: Boolean) - case class GenerateNextBlog( + private case class GenerateNextBlog( startDate: String, endDate: String ) - val dateFormatter = new SimpleDateFormat("yyyy-MM-dd") + private val dateFormatter = new SimpleDateFormat("yyyy-MM-dd") - val archiveDateOps: Opts[String] = + private val archiveDateOps: Opts[String] = Opts .argument[String](metavar = "archiveDate") - val startDateOps: Opts[String] = + private val startDateOps: Opts[String] = Opts .argument[String](metavar = "startDate") - val endDateOps: Opts[String] = + private val endDateOps: Opts[String] = Opts .argument[String](metavar = "endDate") - val publishDateOps: Opts[Option[String]] = + private val publishDateOps: Opts[Option[String]] = Opts .option[String]( "publishdate", @@ -68,7 +68,7 @@ object Main ) .orNone - val archiveFolderOps: Opts[Option[String]] = + private val archiveFolderOps: Opts[Option[String]] = Opts .option[String]( "folder", @@ -77,30 +77,30 @@ object Main ) .orNone - val publishOpts: Opts[Publish] = + private val publishOpts: Opts[Publish] = Opts.subcommand("publish", "Publish next newsletter") { - (publishDateOps, archiveDateOps, archiveFolderOps).mapN(Publish) + (publishDateOps, archiveDateOps, archiveFolderOps).mapN(Publish.apply) } - val createOpts: Opts[Create] = + private val createOpts: Opts[Create] = Opts.subcommand("create", "Create file for next newsletter edition") { Opts .flag("overwrite", "Overwrite next file if it exists", short = "o") .orFalse - .map(Create) + .map(Create.apply) } - val bloggerOpts: Opts[Blogger] = + private val bloggerOpts: Opts[Blogger] = Opts.subcommand("blogger", "Blogger directory tasks") { Opts .flag("directory", "create a new blogger directory page", short = "d") .orFalse - .map(Blogger) + .map(Blogger.apply) } - val generateNextBlogOpts: Opts[GenerateNextBlog] = + private val generateNextBlogOpts: Opts[GenerateNextBlog] = Opts.subcommand("generate", "Generate next blog") { - (startDateOps, endDateOps).mapN(GenerateNextBlog) + (startDateOps, endDateOps).mapN(GenerateNextBlog.apply) } override def main: Opts[IO[ExitCode]] = diff --git a/core/src/main/scala/com/softinio/scalanews/Rome.scala b/core/src/main/scala/com/softinio/scalanews/Rome.scala index b20b087..ea249f2 100644 --- a/core/src/main/scala/com/softinio/scalanews/Rome.scala +++ b/core/src/main/scala/com/softinio/scalanews/Rome.scala @@ -1,5 +1,5 @@ /* - * Copyright 2023 Salar Rahmanian + * Copyright 2024 Salar Rahmanian * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/core/src/main/scala/com/softinio/scalanews/algebra/Article.scala b/core/src/main/scala/com/softinio/scalanews/algebra/Article.scala index 843c06b..bf307b1 100644 --- a/core/src/main/scala/com/softinio/scalanews/algebra/Article.scala +++ b/core/src/main/scala/com/softinio/scalanews/algebra/Article.scala @@ -1,5 +1,5 @@ /* - * Copyright 2023 Salar Rahmanian + * Copyright 2024 Salar Rahmanian * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/core/src/main/scala/com/softinio/scalanews/algebra/Configuration.scala b/core/src/main/scala/com/softinio/scalanews/algebra/Configuration.scala index 91ba565..a734e6f 100644 --- a/core/src/main/scala/com/softinio/scalanews/algebra/Configuration.scala +++ b/core/src/main/scala/com/softinio/scalanews/algebra/Configuration.scala @@ -1,5 +1,5 @@ /* - * Copyright 2023 Salar Rahmanian + * Copyright 2024 Salar Rahmanian * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,14 @@ package com.softinio.scalanews.algebra +import pureconfig._ +import pureconfig.generic.derivation.default._ + import java.net.URI -case class Blog(name: String, url: URI, rss: URI) +final case class Blog(name: String, url: URI, rss: URI) derives ConfigReader +final case class Configuration(bloggers: List[Blog]) derives ConfigReader -case class Configuration(bloggers: List[Blog]) +object Config { + given urlReader: ConfigReader[URI] = ConfigReader[String].map(URI.create) +} diff --git a/core/src/test/scala/com/softinio/scalanews/BloggersSuite.scala b/core/src/test/scala/com/softinio/scalanews/BloggersSuite.scala index 7807caf..3c14a63 100644 --- a/core/src/test/scala/com/softinio/scalanews/BloggersSuite.scala +++ b/core/src/test/scala/com/softinio/scalanews/BloggersSuite.scala @@ -1,5 +1,5 @@ /* - * Copyright 2023 Salar Rahmanian + * Copyright 2024 Salar Rahmanian * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/core/src/test/scala/com/softinio/scalanews/ConfigLoaderSuite.scala b/core/src/test/scala/com/softinio/scalanews/ConfigLoaderSuite.scala index 48b2686..94c68b3 100644 --- a/core/src/test/scala/com/softinio/scalanews/ConfigLoaderSuite.scala +++ b/core/src/test/scala/com/softinio/scalanews/ConfigLoaderSuite.scala @@ -1,5 +1,5 @@ /* - * Copyright 2023 Salar Rahmanian + * Copyright 2024 Salar Rahmanian * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/core/src/test/scala/com/softinio/scalanews/FileHandlerSuite.scala b/core/src/test/scala/com/softinio/scalanews/FileHandlerSuite.scala index 06686b1..5725a7b 100644 --- a/core/src/test/scala/com/softinio/scalanews/FileHandlerSuite.scala +++ b/core/src/test/scala/com/softinio/scalanews/FileHandlerSuite.scala @@ -1,5 +1,5 @@ /* - * Copyright 2023 Salar Rahmanian + * Copyright 2024 Salar Rahmanian * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,50 +18,63 @@ package com.softinio.scalanews import java.time.format.DateTimeFormatter.BASIC_ISO_DATE import java.time.LocalDate -import java.nio.file.Files -import java.nio.file.Path +import fs2.io.file._ import java.nio.charset.StandardCharsets import java.time.format.DateTimeFormatter import java.time.format.FormatStyle -import java.util.stream.Collectors - import munit.CatsEffectSuite import cats.effect._ class FileHandlerSuite extends CatsEffectSuite { - val sampleFile = FunFixture[Path]( + val sampleFile: FunFixture[Path] = FunFixture[Path]( setup = { test => val filename = test.name.replace(" ", "_") - val theFile = Files.createTempFile("tmp", s"${filename}.md") - Files.write(theFile, "# Scala News\n".getBytes(StandardCharsets.UTF_8)) + val content = + fs2.Stream.emits("# Scala News\n".getBytes(StandardCharsets.UTF_8)) + val theFile = Path.apply((s"$filename.md")) + Files[IO].writeAll(theFile)(content).compile.drain.unsafeRunSync() + theFile }, teardown = { file => - Files.deleteIfExists(file) + Files[IO].deleteIfExists(file).unsafeRunSync() () } ) - sampleFile.test("updateFileHeader succesfully") { file => - val updated = FileHandler.updateFileHeader(file, LocalDate.now()) + sampleFile.test("test testfile") { file => + val result = Files[IO] + .readAll(file) + .through(fs2.text.utf8.decode) + .compile + .foldMonoid + .map(_.trim) + assertIO(result, "# Scala News") + } + + sampleFile.test("updateFileHeader successfully") { file => val expectedDate = LocalDate .now() .format(DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG)) val expectedHeader = s"# Scala News - $expectedDate" val result = for { - got <- updated - extracted <- got match { - case Right(path) => - IO( - Files - .lines(path, StandardCharsets.UTF_8) - .collect(Collectors.joining(System.lineSeparator())) - ) + got <- FileHandler.updateFileHeader(file, LocalDate.now()) + exists <- got match { + case Right(path) => { + println(s"got path: $path") + Files[IO] + .readAll(path) + .through(fs2.text.utf8.decode) + .through(fs2.text.lines) + .exists(line => line.contains(expectedHeader)) + .compile + .lastOrError + } case _ => IO.pure("") } - } yield extracted.contains(expectedHeader) + } yield exists assertIO(result, true) } diff --git a/core/src/test/scala/com/softinio/scalanews/HttpClientSuite.scala b/core/src/test/scala/com/softinio/scalanews/HttpClientSuite.scala index 8bedc9f..495ad89 100644 --- a/core/src/test/scala/com/softinio/scalanews/HttpClientSuite.scala +++ b/core/src/test/scala/com/softinio/scalanews/HttpClientSuite.scala @@ -1,5 +1,5 @@ /* - * Copyright 2023 Salar Rahmanian + * Copyright 2024 Salar Rahmanian * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package com.softinio.scalanews +import scala.io.Source.fromInputStream import cats.effect._ import munit.CatsEffectSuite @@ -27,7 +28,7 @@ class HttpClientSuite extends CatsEffectSuite { test("Fetch Rss") { val result = HttpClient.fetchRss("https://www.softinio.com/atom.xml") val obtained = result.use { res => - val resultStr = new String(res.readAllBytes) + val resultStr = fromInputStream(res).mkString IO(resultStr.contains("lightening-talks-at-pybay-2018")) } assertIO(obtained, true) diff --git a/core/src/test/scala/com/softinio/scalanews/RomeSuite.scala b/core/src/test/scala/com/softinio/scalanews/RomeSuite.scala index 71e2d90..0f9994f 100644 --- a/core/src/test/scala/com/softinio/scalanews/RomeSuite.scala +++ b/core/src/test/scala/com/softinio/scalanews/RomeSuite.scala @@ -1,5 +1,5 @@ /* - * Copyright 2023 Salar Rahmanian + * Copyright 2024 Salar Rahmanian * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/flake.lock b/flake.lock index 128cbd7..80c450b 100644 --- a/flake.lock +++ b/flake.lock @@ -6,11 +6,11 @@ "nixpkgs": "nixpkgs" }, "locked": { - "lastModified": 1705240333, - "narHash": "sha256-s9h2h44fCi54sSIT9ktd3eDik9JDpQE9DeYuXcA44u4=", + "lastModified": 1708939976, + "narHash": "sha256-O5+nFozxz2Vubpdl1YZtPrilcIXPcRAjqNdNE8oCRoA=", "owner": "numtide", "repo": "devshell", - "rev": "ca1ff587c602b934afe830ea3cb26d0fbde4c395", + "rev": "5ddecd67edbd568ebe0a55905273e56cc82aabe3", "type": "github" }, "original": { @@ -42,11 +42,11 @@ "systems": "systems_2" }, "locked": { - "lastModified": 1701680307, - "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=", + "lastModified": 1709126324, + "narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=", "owner": "numtide", "repo": "flake-utils", - "rev": "4022d587cbbfd70fe950c1e2083a02621806a725", + "rev": "d465f4819400de7c8d874d50b982301f28a84605", "type": "github" }, "original": { @@ -73,11 +73,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1705242415, - "narHash": "sha256-a8DRYrNrzTudvO7XHUPNJD89Wbf1ZZT0VbwCsPnHWaE=", + "lastModified": 1710097495, + "narHash": "sha256-B7Ea7q7hU7SE8wOPJ9oXEBjvB89yl2csaLjf5v/7jr8=", "owner": "nixos", "repo": "nixpkgs", - "rev": "ea780f3de2d169f982564128804841500e85e373", + "rev": "d40e866b1f98698d454dad8f592fe7616ff705a4", "type": "github" }, "original": { @@ -137,11 +137,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1705527810, - "narHash": "sha256-bl1CkA40UgejXV+ZMAaIqkPvkfl/5aqAgQP09TTrbiQ=", + "lastModified": 1710188850, + "narHash": "sha256-KbNmyxEvcnq5h/wfeL1ZxO9RwoNRjJ0IgYlUZpdSlLo=", "owner": "typelevel", "repo": "typelevel-nix", - "rev": "3ff868a8830e7513a17ac7d2f4f071411c96b35e", + "rev": "60c3868688cb8f5f7ebc781f6e122c061ae35d4d", "type": "github" }, "original": { diff --git a/project/build.properties b/project/build.properties index e8a1e24..04267b1 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.9.7 +sbt.version=1.9.9 diff --git a/project/plugins.sbt b/project/plugins.sbt index a35771e..26c1ea4 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,4 +1,4 @@ addSbtPlugin("org.scalameta" % "sbt-native-image" % "0.3.4") -addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.4.17") -addSbtPlugin("org.typelevel" % "sbt-typelevel-site" % "0.4.17") +addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.6.7") +addSbtPlugin("org.typelevel" % "sbt-typelevel-site" % "0.6.7") addDependencyTreePlugin