Repository: scala-steward-org/scala-steward
Pull request #1925 (2021-02-06 09:14:38)

Remove effect from JsonKeyValueStore#jsonFile
It is unnecessary to compute the root directory on every call to
`jsonFile`. This change does it only once at the creation of the store.


Patch:
diff --git a/docs/running.md b/docs/running.md
index 58c2a0e9..9c697bbb 100644
--- a/docs/running.md
+++ b/docs/running.md
@@ -94,13 +94,13 @@ run --do-not-fork --workspace "/path/workspace" --repos-file "/path/repos.md" --
 ```
 
 
-### Running on Docker for Bitbucket
+#### Running on Docker for Bitbucket
 
 * Create a file `repos.md` that will be injected into the container as as volume.
 * Create a file `run.sh` with this content:
 
 ```
-echo "#!/bin/sh"                  >> pass.sh
+echo "#!/bin/sh"                  >> pass.sh  
 echo "echo '$BITBUCKET_PASSWORD'" >> pass.sh
 
 chmod +x pass.sh
@@ -118,72 +118,13 @@ docker run -v $PWD:/opt/scala-steward \
     --vcs-type "bitbucket" \
     --vcs-api-host "https://api.bitbucket.org/2.0" \
     --vcs-login "$BITBUCKET_USERNAME"
+    
 ```
 
 * Run it from a CI tool or manually using with this command:
 
 `BITBUCKET_USERNAME=<myuser> BITBUCKET_PASSWORD=<mypass> ./run.sh`
 
-### Running in a Bitbucket pipeline to update Bitbucket repos
-
-* Create a file `repos.md` that will be injected into the container as a volume.
-* Create a file `run.sh` with this content:
-
-```
-echo "#!/bin/sh"                  >> pass.sh
-echo "echo '$BITBUCKET_PASSWORD'" >> pass.sh
-
-chmod +x pass.sh
-
-docker run -v $PWD:/opt/scala-steward \
-    -i fthomas/scala-steward:latest \
-    --env-var LOG_LEVEL=TRACE \
-    --do-not-fork \
-    --workspace "/opt/scala-steward/workspace" \
-    --repos-file "/opt/scala-steward/repos.md" \
-    --default-repo-conf "/opt/scala-steward/default.scala-steward.conf" \
-    --git-ask-pass "/opt/scala-steward/pass.sh" \
-    --git-author-email "myemail@company.xyz" \
-    --vcs-type "bitbucket" \
-    --vcs-api-host "https://api.bitbucket.org/2.0" \
-    --vcs-login "$BITBUCKET_USERNAME"
-```
-
-NOTE: This script is slightly different to the one in the previous Bitbucket
-example, because it needs to run in a Bitbucket Pipeline. The `-t` flag has been
-removed, and we do mount `~/.sbt` as a volume.
-
-* Prepare an S3 bucket (or similar storage) to persist the Scala Steward
-  workspace between runs
-* Set some repository variables: AWS credentials, plus the S3 bucket name
-* Create a pipeline to run Scala Steward and sync the workspace to S3:
-
-```
-image:
-  name: <any Linux image with AWS CLI installed>
-
-options:
-  docker: true
-
-definitions:
-  services:
-    docker:
-      memory: 4096
-
-pipelines:
-  custom:
-    run-scala-steward:
-      - step:
-          name: Run Scala Steward
-          size: 2x
-          script:
-            - aws s3 sync s3://${WORKSPACE_BUCKET}/workspace ./workspace
-            - ./run.sh
-            - aws s3 sync ./workspace s3://${WORKSPACE_BUCKET}/workspace
-```
-
-* In the Pipelines UI, configure the pipeline to run on a schedule (e.g. daily)
-
 ### Running On-premise
 
 #### GitHub Enterprise
diff --git a/modules/core/src/main/scala/org/scalasteward/core/application/Context.scala b/modules/core/src/main/scala/org/scalasteward/core/application/Context.scala
index 68213b80..c1f162e6 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/application/Context.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/application/Context.scala
@@ -35,7 +35,7 @@ import org.scalasteward.core.git.{GenGitAlg, GitAlg}
 import org.scalasteward.core.io.{FileAlg, ProcessAlg, WorkspaceAlg}
 import org.scalasteward.core.nurture.{NurtureAlg, PullRequestData, PullRequestRepository}
 import org.scalasteward.core.persistence.{CachingKeyValueStore, JsonKeyValueStore}
-import org.scalasteward.core.repocache.{RefreshErrorAlg, RepoCacheAlg, RepoCacheRepository}
+import org.scalasteward.core.repocache._
 import org.scalasteward.core.repoconfig.RepoConfigAlg
 import org.scalasteward.core.scalafix.{MigrationAlg, MigrationsLoader}
 import org.scalasteward.core.scalafmt.ScalafmtAlg
@@ -108,9 +108,15 @@ object Context {
         migrationsLoader.loadAll(config.scalafixCfg).map(new MigrationAlg(_))
       implicit0(urlChecker: UrlChecker[F]) <- UrlChecker.create[F](config)
       kvsPrefix = Some(config.vcsType.asString)
-      pullRequestsStore <- CachingKeyValueStore.wrap(
-        new JsonKeyValueStore[F, Repo, Map[Uri, PullRequestData]]("pull_requests", "2", kvsPrefix)
-      )
+      pullRequestsStore <- JsonKeyValueStore
+        .create[F, Repo, Map[Uri, PullRequestData]]("pull_requests", "2", kvsPrefix)
+        .flatMap(CachingKeyValueStore.wrap(_))
+      refreshErrorStore <- JsonKeyValueStore
+        .create[F, Repo, RefreshErrorAlg.Entry]("refresh_error", "1", kvsPrefix)
+      repoCacheStore <- JsonKeyValueStore
+        .create[F, Repo, RepoCache]("repo_cache", "1", kvsPrefix)
+      versionsStore <- JsonKeyValueStore
+        .create[F, VersionsCache.Key, VersionsCache.Value]("versions", "2")
     } yield {
       implicit val dateTimeAlg: DateTimeAlg[F] = DateTimeAlg.create[F]
       implicit val repoConfigAlg: RepoConfigAlg[F] = new RepoConfigAlg[F](config)
@@ -120,7 +126,7 @@ object Context {
       implicit val hookExecutor: HookExecutor[F] = new HookExecutor[F]
       implicit val httpJsonClient: HttpJsonClient[F] = new HttpJsonClient[F]
       implicit val repoCacheRepository: RepoCacheRepository[F] =
-        new RepoCacheRepository[F](new JsonKeyValueStore("repo_cache", "1", kvsPrefix))
+        new RepoCacheRepository[F](repoCacheStore)
       implicit val vcsApiAlg: VCSApiAlg[F] = new VCSSelection[F](config, vcsUser).vcsApiAlg
       implicit val vcsRepoAlg: VCSRepoAlg[F] = new VCSRepoAlg[F](config)
       implicit val vcsExtraAlg: VCSExtraAlg[F] = VCSExtraAlg.create[F](config)
@@ -130,14 +136,13 @@ object Context {
       implicit val selfCheckAlg: SelfCheckAlg[F] = new SelfCheckAlg[F]
       implicit val coursierAlg: CoursierAlg[F] = CoursierAlg.create[F]
       implicit val versionsCache: VersionsCache[F] =
-        new VersionsCache[F](config.cacheTtl, new JsonKeyValueStore("versions", "2"))
+        new VersionsCache[F](config.cacheTtl, versionsStore)
       implicit val updateAlg: UpdateAlg[F] = new UpdateAlg[F]
       implicit val mavenAlg: MavenAlg[F] = MavenAlg.create[F](config)
       implicit val sbtAlg: SbtAlg[F] = SbtAlg.create[F](config)
       implicit val millAlg: MillAlg[F] = MillAlg.create[F]
       implicit val buildToolDispatcher: BuildToolDispatcher[F] = BuildToolDispatcher.create[F]
-      implicit val refreshErrorAlg: RefreshErrorAlg[F] =
-        new RefreshErrorAlg[F](new JsonKeyValueStore("refresh_error", "1", kvsPrefix))
+      implicit val refreshErrorAlg: RefreshErrorAlg[F] = new RefreshErrorAlg[F](refreshErrorStore)
       implicit val repoCacheAlg: RepoCacheAlg[F] = new RepoCacheAlg[F](config)
       implicit val editAlg: EditAlg[F] = new EditAlg[F]
       implicit val nurtureAlg: NurtureAlg[F] = new NurtureAlg[F](config)
diff --git a/modules/core/src/main/scala/org/scalasteward/core/persistence/JsonKeyValueStore.scala b/modules/core/src/main/scala/org/scalasteward/core/persistence/JsonKeyValueStore.scala
index f4b4cd9a..a1c6a7f2 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/persistence/JsonKeyValueStore.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/persistence/JsonKeyValueStore.scala
@@ -25,42 +25,53 @@ import io.circe.syntax._
 import io.circe.{Decoder, Encoder, KeyEncoder}
 import org.scalasteward.core.io.{FileAlg, WorkspaceAlg}
 
-final class JsonKeyValueStore[F[_], K, V](
-    name: String,
-    schemaVersion: String,
-    maybePrefix: Option[String] = None
-)(implicit
+final class JsonKeyValueStore[F[_], K, V](storeRoot: File, name: String)(implicit
     fileAlg: FileAlg[F],
     keyEncoder: KeyEncoder[K],
     logger: Logger[F],
     valueDecoder: Decoder[V],
     valueEncoder: Encoder[V],
-    workspaceAlg: WorkspaceAlg[F],
     F: Monad[F]
 ) extends KeyValueStore[F, K, V] {
-  override def get(key: K): F[Option[V]] =
-    jsonFile(key).flatMap { file =>
-      fileAlg.readFile(file).flatMap {
-        case None => F.pure(Option.empty[V])
-        case Some(content) =>
-          decode[Option[V]](content) match {
-            case Right(maybeValue) => F.pure(maybeValue)
-            case Left(error) =>
-              logger.error(error)(s"Failed to parse or decode JSON from $file").as(Option.empty[V])
-          }
-      }
+  override def get(key: K): F[Option[V]] = {
+    val file = jsonFile(key)
+    fileAlg.readFile(file).flatMap {
+      case None => F.pure(Option.empty[V])
+      case Some(content) =>
+        decode[Option[V]](content) match {
+          case Right(maybeValue) => F.pure(maybeValue)
+          case Left(error) =>
+            logger.error(error)(s"Failed to parse or decode JSON from $file").as(Option.empty[V])
+        }
     }
+  }
 
-  override def set(key: K, value: Option[V]): F[Unit] =
-    jsonFile(key).flatMap { file =>
-      value match {
-        case Some(v) => fileAlg.writeFile(file, v.asJson.toString)
-        case None    => fileAlg.deleteForce(file)
-      }
+  override def set(key: K, value: Option[V]): F[Unit] = {
+    val file = jsonFile(key)
+    value match {
+      case Some(v) => fileAlg.writeFile(file, v.asJson.spaces2)
+      case None    => fileAlg.deleteForce(file)
     }
+  }
+
+  private def jsonFile(key: K): File =
+    storeRoot / keyEncoder(key) / s"$name.json"
+}
 
-  private def jsonFile(key: K): F[File] = {
-    val keyPath = maybePrefix.fold("")(_ + "/") + keyEncoder(key)
-    workspaceAlg.rootDir.map(_ / "store" / name / s"v$schemaVersion" / keyPath / s"$name.json")
+object JsonKeyValueStore {
+  def create[F[_], K, V](name: String, schemaVersion: String, maybePrefix: Option[String] = None)(
+      implicit
+      fileAlg: FileAlg[F],
+      keyEncoder: KeyEncoder[K],
+      logger: Logger[F],
+      valueDecoder: Decoder[V],
+      valueEncoder: Encoder[V],
+      workspaceAlg: WorkspaceAlg[F],
+      F: Monad[F]
+  ): F[JsonKeyValueStore[F, K, V]] = {
+    val prefix = maybePrefix.fold("")("/" + _)
+    workspaceAlg.rootDir
+      .map(_ / "store" / name / s"v$schemaVersion$prefix")
+      .map(storeRoot => new JsonKeyValueStore(storeRoot, name))
   }
 }
diff --git a/project/Dependencies.scala b/project/Dependencies.scala
index 767979a5..70654be8 100644
--- a/project/Dependencies.scala
+++ b/project/Dependencies.scala
@@ -3,7 +3,7 @@ import sbt.Keys._
 import sbt.librarymanagement.syntax.ExclusionRule
 
 object Dependencies {
-  val attoCore = "org.tpolecat" %% "atto-core" % "0.9.1"
+  val attoCore = "org.tpolecat" %% "atto-core" % "0.9.0"
   val bcprovJdk15to18 = "org.bouncycastle" % "bcprov-jdk15to18" % "1.68"
   val betterFiles = "com.github.pathikrit" %% "better-files" % "3.9.1"
   val betterMonadicFor = "com.olegpy" %% "better-monadic-for" % "0.3.1"
@@ -24,7 +24,7 @@ object Dependencies {
   val disciplineMunit = "org.typelevel" %% "discipline-munit" % "1.0.5"
   val fs2Core = "co.fs2" %% "fs2-core" % "2.5.0"
   val fs2Io = "co.fs2" %% "fs2-io" % fs2Core.revision
-  val http4sCore = "org.http4s" %% "http4s-core" % "0.21.18"
+  val http4sCore = "org.http4s" %% "http4s-core" % "0.21.16"
   val http4sCirce = "org.http4s" %% "http4s-circe" % http4sCore.revision
   val http4sClient = "org.http4s" %% "http4s-client" % http4sCore.revision
   val http4sDsl = "org.http4s" %% "http4s-dsl" % http4sCore.revision


Test patch:
diff --git a/modules/core/src/test/scala/org/scalasteward/core/persistence/JsonKeyValueStoreTest.scala b/modules/core/src/test/scala/org/scalasteward/core/persistence/JsonKeyValueStoreTest.scala
index 9e2bec5f..218a26f2 100644
--- a/modules/core/src/test/scala/org/scalasteward/core/persistence/JsonKeyValueStoreTest.scala
+++ b/modules/core/src/test/scala/org/scalasteward/core/persistence/JsonKeyValueStoreTest.scala
@@ -8,8 +8,8 @@ import org.scalasteward.core.mock.{MockEff, MockState}
 
 class JsonKeyValueStoreTest extends FunSuite {
   test("put, get") {
-    val kvStore = new JsonKeyValueStore[MockEff, String, String]("test", "0")
     val p = for {
+      kvStore <- JsonKeyValueStore.create[MockEff, String, String]("test", "0")
       _ <- kvStore.put("k1", "v1")
       v1 <- kvStore.get("k1")
       _ <- kvStore.put("k2", "v2")
@@ -37,8 +37,8 @@ class JsonKeyValueStoreTest extends FunSuite {
   }
 
   test("modifyF, get, set") {
-    val kvStore = new JsonKeyValueStore[MockEff, String, String]("test", "0")
     val p = for {
+      kvStore <- JsonKeyValueStore.create[MockEff, String, String]("test", "0")
       _ <- kvStore.modifyF("k1")(_ => Option("v0").pure[MockEff])
       v1 <- kvStore.get("k1")
       _ <- kvStore.set("k1", None)
@@ -60,9 +60,9 @@ class JsonKeyValueStoreTest extends FunSuite {
 
   test("cached") {
     val p = for {
-      kvStore <- CachingKeyValueStore.wrap(
-        new JsonKeyValueStore[MockEff, String, String]("test", "0")
-      )
+      kvStore <- JsonKeyValueStore
+        .create[MockEff, String, String]("test", "0")
+        .flatMap(CachingKeyValueStore.wrap(_))
       _ <- kvStore.put("k1", "v1")
       v1 <- kvStore.get("k1")
       v2 <- kvStore.get("k2")