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..b009f4e 100644 --- a/build.sbt +++ b/build.sbt @@ -1,17 +1,18 @@ -import laika.ast._ -import laika.ast.Path._ -import laika.ast.InternalTarget -import laika.helium.Helium +import laika.ast.Path.* + import laika.helium.config.Favicon import laika.helium.config.HeliumIcon import laika.helium.config.IconLink +Global / excludeLintKeys += ThisBuild / nativeImageJvm +Global / excludeLintKeys += ThisBuild / nativeImageVersion + // https://typelevel.org/sbt-typelevel/faq.html#what-is-a-base-version-anyway 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 +26,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,59 +39,50 @@ 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.") } ) .enablePlugins(NativeImagePlugin) lazy val docs = project .in(file("site")) .settings( - tlSiteRelatedProjects := Seq( - TypelevelProject.CatsEffect, - "sbt-typelevel" -> url("https://github.com/typelevel/sbt-typelevel"), - "decline" -> url("https://ben.kirw.in/decline/"), - "Laika" -> url("https://planet42.github.io/Laika/") - ), - tlSiteHeliumConfig := { - tlSiteHeliumConfig.value.all + tlSiteHelium := { + tlSiteHelium.value.all .metadata( title = Some("Scala News"), language = Some("en") ) .site .topNavigationBar( - homeLink = IconLink.internal(Root / "index.md", HeliumIcon.home), - navLinks = Seq( - IconLink.external( - "https://github.com/softinio/scalanews", - HeliumIcon.github - ) - ) + homeLink = IconLink.internal(Root / "index.md", HeliumIcon.home) ) .site .favIcons( Favicon.internal(Root / "img/favicon-32x32.png", sizes = "32x32") ) + .site + .footer( + "
\n Created by Salar Rahmanian and Contributors.\n
\n \"Creative
The content on this site by Salar Rahmanian and contributors is licensed under a Creative Commons Attribution 4.0 International License.
\n Made with ❤\uFE0F in San Francisco using: | cats-effect | | sbt-typelevel | | decline | | Laika | " + ) } ) .enablePlugins(TypelevelSitePlugin) diff --git a/core/.jvm/.github/workflows/ci.yml b/core/.jvm/.github/workflows/ci.yml new file mode 100644 index 0000000..abc3b3e --- /dev/null +++ b/core/.jvm/.github/workflows/ci.yml @@ -0,0 +1,141 @@ +# This file was automatically generated by sbt-github-actions using the +# githubWorkflowGenerate task. You should add and commit this file to +# your git repository. It goes without saying that you shouldn't edit +# this file by hand! Instead, if you wish to make changes, you should +# change your sbt build configuration to revise the workflow description +# to meet your needs, then regenerate this file. + +name: Continuous Integration + +on: + pull_request: + branches: ['**', '!update/**', '!pr/**'] + push: + branches: ['**', '!update/**', '!pr/**'] + tags: [v*] + +env: + 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: [3] + java: [corretto@21] + project: [rootJVM] + runs-on: ${{ matrix.os }} + timeout-minutes: 60 + 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@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: Check that workflows are up to date + run: sbt githubWorkflowCheck + + - name: Check headers and formatting + 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' && matrix.os == 'ubuntu-latest' + run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' mimaReportBinaryIssues + + - name: Generate API documentation + if: matrix.java == 'corretto@21' && matrix.os == 'ubuntu-latest' + run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' doc + + dependency-submission: + name: Submit Dependencies + if: github.event_name != 'pull_request' + 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@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@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: Generate site + run: sbt docs/tlSite + + - name: Publish site + if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' + uses: peaceiris/actions-gh-pages@v3.9.3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: site/target/docs/site + keep_files: true diff --git a/core/.jvm/.github/workflows/clean.yml b/core/.jvm/.github/workflows/clean.yml new file mode 100644 index 0000000..547aaa4 --- /dev/null +++ b/core/.jvm/.github/workflows/clean.yml @@ -0,0 +1,59 @@ +# This file was automatically generated by sbt-github-actions using the +# githubWorkflowGenerate task. You should add and commit this file to +# your git repository. It goes without saying that you shouldn't edit +# this file by hand! Instead, if you wish to make changes, you should +# change your sbt build configuration to revise the workflow description +# to meet your needs, then regenerate this file. + +name: Clean + +on: push + +jobs: + delete-artifacts: + name: Delete Artifacts + runs-on: ubuntu-latest + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - name: Delete artifacts + run: | + # Customize those three lines with your repository and credentials: + REPO=${GITHUB_API_URL}/repos/${{ github.repository }} + + # A shortcut to call GitHub API. + ghapi() { curl --silent --location --user _:$GITHUB_TOKEN "$@"; } + + # A temporary file which receives HTTP response headers. + TMPFILE=/tmp/tmp.$$ + + # An associative array, key: artifact name, value: number of artifacts of that name. + declare -A ARTCOUNT + + # Process all artifacts on this repository, loop on returned "pages". + URL=$REPO/actions/artifacts + while [[ -n "$URL" ]]; do + + # Get current page, get response headers in a temporary file. + JSON=$(ghapi --dump-header $TMPFILE "$URL") + + # Get URL of next page. Will be empty if we are at the last page. + URL=$(grep '^Link:' "$TMPFILE" | tr ',' '\n' | grep 'rel="next"' | head -1 | sed -e 's/.*.*//') + rm -f $TMPFILE + + # Number of artifacts on this page: + COUNT=$(( $(jq <<<$JSON -r '.artifacts | length') )) + + # Loop on all artifacts on this page. + for ((i=0; $i < $COUNT; i++)); do + + # Get name of artifact and count instances of this name. + name=$(jq <<<$JSON -r ".artifacts[$i].name?") + ARTCOUNT[$name]=$(( $(( ${ARTCOUNT[$name]} )) + 1)) + + id=$(jq <<<$JSON -r ".artifacts[$i].id?") + size=$(( $(jq <<<$JSON -r ".artifacts[$i].size_in_bytes?") )) + printf "Deleting '%s' #%d, %'d bytes\n" $name ${ARTCOUNT[$name]} $size + ghapi -X DELETE $REPO/actions/artifacts/$id + done + done diff --git a/core/src/main/scala/com/softinio/scalanews/Bloggers.scala b/core/src/main/scala/com/softinio/scalanews/Bloggers.scala index 48d3414..fc535dd 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,22 +16,21 @@ 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 cats.effect.* +import fs2.io.file.* +import fs2.Stream import com.rometools.rome.feed.synd.SyndEntry -import scala.jdk.CollectionConverters._ +import scala.jdk.CollectionConverters.* import com.softinio.scalanews.algebra.Article 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..3d8ba28 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. @@ -16,11 +16,9 @@ package com.softinio.scalanews -import pureconfig._ -import pureconfig.generic.auto._ -import pureconfig.module.catseffect.syntax._ +import pureconfig.* +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..7d70bed 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,53 @@ 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 cats.effect._ -import cats.nio.file.Files +import fs2.text +import cats.effect.* +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" + + val tempFilePath: Resource[IO, Path] = + Files[IO].tempFile + + tempFilePath.use { usingTempFile => + 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,40 +87,42 @@ 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) + } 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 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 { - case Right(rDate) => IO(s"scala_news_${rDate}.md") + case Right(rDate) => IO(s"scala_news_$rDate.md") case _ => IO("") } - } yield (fileName) + } 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], @@ -127,5 +146,5 @@ object FileHandler { _ <- if (nextExists) Files[IO].move(nextFilePath, indexFilePath) else IO.unit _ <- create(false) - } yield (ExitCode.Success) + } yield ExitCode.Success } diff --git a/core/src/main/scala/com/softinio/scalanews/HttpClient.scala b/core/src/main/scala/com/softinio/scalanews/HttpClient.scala index 4cdab04..65afdce 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. @@ -16,14 +16,14 @@ package com.softinio.scalanews -import cats.effect._ +import cats.effect.* import org.http4s.client.Client import org.http4s.ember.client.EmberClientBuilder import java.io.InputStream import org.http4s.Request import org.http4s.Method import org.http4s.Uri -import fs2._ +import fs2.* object HttpClient { diff --git a/core/src/main/scala/com/softinio/scalanews/Main.scala b/core/src/main/scala/com/softinio/scalanews/Main.scala index 6be70c4..09d3de4 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. @@ -18,11 +18,11 @@ package com.softinio.scalanews import java.text.SimpleDateFormat -import cats.effect._ -import cats.implicits._ +import cats.effect.* +import cats.implicits.* -import com.monovore.decline._ -import com.monovore.decline.effect._ +import com.monovore.decline.* +import com.monovore.decline.effect.* object Main extends CommandIOApp( @@ -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]] = @@ -114,13 +114,12 @@ object Main dateFormatter.parse(startDate), dateFormatter.parse(endDate) ) - case Blogger(directory) => { + case Blogger(directory) => if (directory) { for { config <- ConfigLoader.load() result <- Bloggers.createBloggerDirectory(config.bloggers) - } yield (result) + } yield result } else IO(ExitCode.Success) - } } } diff --git a/core/src/main/scala/com/softinio/scalanews/Rome.scala b/core/src/main/scala/com/softinio/scalanews/Rome.scala index b20b087..61cac58 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. @@ -18,7 +18,7 @@ package com.softinio.scalanews import java.io.InputStream -import cats.effect._ +import cats.effect.* import com.rometools.rome.feed.synd.SyndFeed import com.rometools.rome.io.SyndFeedInput @@ -34,5 +34,5 @@ object Rome { }.attempt def fetchFeed(feedUrl: String): IO[Either[Throwable, SyndFeed]] = - HttpClient.fetchRss(feedUrl).use(parseFeed(_)) + HttpClient.fetchRss(feedUrl).use(parseFeed) } 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..64f1472 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..39d50b6 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. @@ -23,10 +23,10 @@ import java.nio.charset.StandardCharsets import munit.CatsEffectSuite class ConfigLoaderSuite extends CatsEffectSuite { - val sampleConfig = FunFixture[Path]( + val sampleConfig: FunFixture[Path] = FunFixture[Path]( setup = { test => val filename = test.name.replace(" ", "_") - val theFile = Files.createTempFile("tmp", s"${filename}.json") + val theFile = Files.createTempFile("tmp", s"$filename.json") val sampleJson = """ { "bloggers": [ @@ -48,7 +48,7 @@ class ConfigLoaderSuite extends CatsEffectSuite { ) sampleConfig.test("test loading json config") { file => val result = for { - conf <- ConfigLoader.load(file.toString()) + conf <- ConfigLoader.load(file.toString) } yield conf.bloggers.head.name == "Salar Rahmanian" assertIO(result, true) } diff --git a/core/src/test/scala/com/softinio/scalanews/FileHandlerSuite.scala b/core/src/test/scala/com/softinio/scalanews/FileHandlerSuite.scala index 06686b1..5a2293b 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,61 @@ 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._ +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 { + got <- FileHandler.updateFileHeader(file, LocalDate.now()) + exists <- got match { case Right(path) => - IO( - Files - .lines(path, StandardCharsets.UTF_8) - .collect(Collectors.joining(System.lineSeparator())) - ) + 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..3f18829 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,7 +16,8 @@ package com.softinio.scalanews -import cats.effect._ +import scala.io.Source.fromInputStream +import cats.effect.* import munit.CatsEffectSuite import cats.effect.unsafe.IORuntime @@ -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..e39dfa7 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. @@ -16,7 +16,7 @@ package com.softinio.scalanews -import cats.effect._ +import cats.effect.* import munit.CatsEffectSuite @@ -27,10 +27,9 @@ class RomeSuite extends CatsEffectSuite { result <- Rome.fetchFeed("https://www.softinio.com/atom.xml") } yield { result match { - case Right(feed) => { + case Right(feed) => val title = feed.getTitle title == "Salar Rahmanian" - } case _ => false } } diff --git a/docs/default.template.html b/docs/default.template.html index 0525f5b..c98b8c1 100644 --- a/docs/default.template.html +++ b/docs/default.template.html @@ -1,101 +1,28 @@ - - - - - - ${cursor.currentDocument.title} - @:for(laika.site.metadata.authors) - - @:@ - @:for(laika.site.metadata.description) - - @:@ - @:for(helium.favIcons) - - @:@ - @:for(helium.webFonts) - - @:@ - @:linkCSS { paths = ${helium.site.includeCSS} } - @:linkJS { paths = ${helium.site.includeJS} } - @:heliumInitVersions - @:heliumInitPreview(container) - - - +@:include(helium.site.templates.head) -
+ -
- - @:icon(navigationMenu) - - @:for(laika.versions) - - @:@ -
+@:include(helium.site.templates.topNav) - ${?helium.topBar.home} +@:include(helium.site.templates.mainNav) - ${?helium.topBar.links} +
-
+ @:include(helium.site.templates.pageNav) - + -
+
- - -
- - ${cursor.currentDocument.content} - -
- -
- - - + + \ No newline at end of file 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..57b5ef8 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.7.0") +addSbtPlugin("org.typelevel" % "sbt-typelevel-site" % "0.7.0") addDependencyTreePlugin