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
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/.*/' -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 @@
-