diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml deleted file mode 100644 index d744427..0000000 --- a/.github/workflows/ci.yaml +++ /dev/null @@ -1,68 +0,0 @@ -name: CI -on: [push, pull_request] -env: - CI: true - CI_SNAPSHOT_RELEASE: +publishSigned - SCALA_VERSION: 2.12.12 -jobs: - validate: - name: Scala ${{ matrix.scala }}, Java ${{ matrix.java }} - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - java: [adopt@1.8, adopt@1.11, adopt@1.14] - scala: [2.12.12, 2.13.3] - env: - SCALA_VERSION: ${{ matrix.scala }} - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - uses: olafurpg/setup-scala@v10 - with: - java-version: ${{ matrix.java }} - - name: Cache Coursier - uses: actions/cache@v1 - with: - path: ~/.cache/coursier - key: sbt-coursier-cache - - name: Cache SBT - uses: actions/cache@v1 - with: - path: ~/.sbt - key: sbt-${{ hashFiles('**/build.sbt') }} - - name: Compile - run: sbt ++$SCALA_VERSION test:compile - - name: Check compatibility - run: sbt ++$SCALA_VERSION mimaReportBinaryIssues - - name: Test - run: sbt ++$SCALA_VERSION test - - name: Scaladoc - run: sbt ++$SCALA_VERSION doc - docs: - name: Doc Site - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: olafurpg/setup-scala@v10 - - name: Cache Coursier - uses: actions/cache@v1 - with: - path: ~/.cache/coursier - key: sbt-coursier-cache - - name: Cache SBT - uses: actions/cache@v1 - with: - path: ~/.sbt - key: sbt-${{ hashFiles('**/build.sbt') }} - - name: Set up Ruby 2.6 - uses: actions/setup-ruby@v1 - with: - ruby-version: 2.6 - - name: Install Jekyll - run: | - gem install bundler - bundle install --gemfile=site/Gemfile - - name: Build project site - run: sbt ++$SCALA_VERSION site/makeMicrosite \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..de4c570 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,126 @@ +# 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: ['*'] + push: + branches: ['*'] + tags: [v*] + +env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + +jobs: + build: + name: Build and Test + strategy: + matrix: + os: [ubuntu-latest] + scala: [2.12.13, 2.13.4, 3.0.0-M2, 3.0.0-M3] + java: [adopt@1.8] + runs-on: ${{ matrix.os }} + steps: + - name: Checkout current branch (full) + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Setup Java and Scala + uses: olafurpg/setup-scala@v10 + with: + java-version: ${{ matrix.java }} + + - 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: Setup Ruby + if: matrix.scala == '2.13.4' + uses: ruby/setup-ruby@v1 + with: + ruby-version: 2.6.0 + + - name: Install microsite dependencies + if: matrix.scala == '2.13.4' + run: | + gem install saas + gem install jekyll -v 3.2.1 + + - name: Check that workflows are up to date + run: sbt ++${{ matrix.scala }} githubWorkflowCheck + + - run: sbt ++${{ matrix.scala }} test mimaReportBinaryIssues + + - if: matrix.scala == '2.13.4' + run: sbt ++${{ matrix.scala }} site/makeMicrosite + + publish: + name: Publish Artifacts + needs: [build] + if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v')) + strategy: + matrix: + os: [ubuntu-latest] + scala: [3.0.0-M3] + java: [adopt@1.8] + runs-on: ${{ matrix.os }} + steps: + - name: Checkout current branch (full) + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Setup Java and Scala + uses: olafurpg/setup-scala@v10 + with: + java-version: ${{ matrix.java }} + + - 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') }} + + - uses: olafurpg/setup-gpg@v3 + + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 2.6.0 + + - name: Install microsite dependencies + run: | + gem install saas + gem install jekyll -v 3.2.1 + + - name: Publish artifacts to Sonatype + env: + PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} + PGP_SECRET: ${{ secrets.PGP_SECRET }} + SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} + SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} + run: sbt ++${{ matrix.scala }} ci-release + + - name: Publish microsite + run: sbt ++${{ matrix.scala }} ++2.13.4 site/publishMicrosite \ No newline at end of file diff --git a/.github/workflows/clean.yml b/.github/workflows/clean.yml new file mode 100644 index 0000000..b535fcc --- /dev/null +++ b/.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 \ No newline at end of file diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml deleted file mode 100644 index 9672380..0000000 --- a/.github/workflows/release.yaml +++ /dev/null @@ -1,50 +0,0 @@ -name: Release -on: - push: - branches: - - canon - tags: - - '*' -env: - CI: true - SCALA_VERSION: 2.12.12 -jobs: - release: - name: Release - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - with: - fetch-depth: 0 - - uses: olafurpg/setup-scala@v10 - - uses: olafurpg/setup-gpg@v3 - - name: Cache Coursier - uses: actions/cache@v1 - with: - path: ~/.cache/coursier - key: sbt-coursier-cache - - name: Cache SBT - uses: actions/cache@v1 - with: - path: ~/.sbt - key: sbt-${{ hashFiles('**/build.sbt') }} - - name: Publish - run: sbt ci-release - env: - PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} - PGP_SECRET: ${{ secrets.PGP_SECRET }} - SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} - SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} - - name: Set up Ruby 2.6 - uses: actions/setup-ruby@v1 - with: - ruby-version: 2.6 - - name: Install Jekyll - run: | - gem install bundler - bundle install --gemfile=site/Gemfile - - name: Publish microsite - run: | - sbt ++$SCALA_VERSION site/publishMicrosite - env: - GITHUB_TOKEN: ${{ secrets.SITE_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 86a261c..254e649 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ tags .bloop .metals metals.sbt -.vscode \ No newline at end of file +.vscode +.bsp \ No newline at end of file diff --git a/build.sbt b/build.sbt index 0629d47..5f3638f 100644 --- a/build.sbt +++ b/build.sbt @@ -1,18 +1,75 @@ import sbtcrossproject.CrossPlugin.autoImport.{crossProject, CrossType} -val catsV = "2.2.0" -val catsEffectV = "2.2.0" -val shapelessV = "2.3.3" -val fs2V = "2.4.2" -val http4sV = "0.21.6" -val circeV = "0.13.0" -val doobieV = "0.9.0" -val log4catsV = "1.1.1" +val catsV = "2.3.1" +val catsEffectV = "2.3.1" +val fs2V = "2.5.0" + +val munitCatsEffectV = "0.12.0" val specs2V = "4.10.1" -val kindProjectorV = "0.11.0" +val kindProjectorV = "0.11.2" val betterMonadicForV = "0.3.1" +val Scala213 = "2.13.4" + +ThisBuild / crossScalaVersions := Seq("2.12.13", Scala213, "3.0.0-M2", "3.0.0-M3") +ThisBuild / scalaVersion := crossScalaVersions.value.last + +ThisBuild / githubWorkflowArtifactUpload := false + +val Scala213Cond = s"matrix.scala == '$Scala213'" + +def rubySetupSteps(cond: Option[String]) = Seq( + WorkflowStep.Use( + "ruby", "setup-ruby", "v1", + name = Some("Setup Ruby"), + params = Map("ruby-version" -> "2.6.0"), + cond = cond), + + WorkflowStep.Run( + List( + "gem install saas", + "gem install jekyll -v 3.2.1"), + name = Some("Install microsite dependencies"), + cond = cond)) + +ThisBuild / githubWorkflowBuildPreamble ++= + rubySetupSteps(Some(Scala213Cond)) + +ThisBuild / githubWorkflowBuild := Seq( + WorkflowStep.Sbt(List("test", "mimaReportBinaryIssues")), + + WorkflowStep.Sbt( + List("site/makeMicrosite"), + cond = Some(Scala213Cond))) + +ThisBuild / githubWorkflowTargetTags ++= Seq("v*") + +// currently only publishing tags +ThisBuild / githubWorkflowPublishTargetBranches := + Seq(RefPredicate.StartsWith(Ref.Tag("v"))) + +ThisBuild / githubWorkflowPublishPreamble ++= + WorkflowStep.Use("olafurpg", "setup-gpg", "v3") +: rubySetupSteps(None) + +ThisBuild / githubWorkflowPublish := Seq( + WorkflowStep.Sbt( + List("ci-release"), + name = Some("Publish artifacts to Sonatype"), + env = Map( + "PGP_PASSPHRASE" -> "${{ secrets.PGP_PASSPHRASE }}", + "PGP_SECRET" -> "${{ secrets.PGP_SECRET }}", + "SONATYPE_PASSWORD" -> "${{ secrets.SONATYPE_PASSWORD }}", + "SONATYPE_USERNAME" -> "${{ secrets.SONATYPE_USERNAME }}")), + + WorkflowStep.Sbt( + List(s"++$Scala213", "site/publishMicrosite"), + name = Some("Publish microsite") + ) +) + + + // Projects lazy val `rediculous` = project.in(file(".")) .disablePlugins(MimaPlugin) @@ -65,15 +122,6 @@ lazy val site = project.in(file("site")) "gray-lighter" -> "#F4F3F4", "white-color" -> "#FFFFFF" ), - micrositeCompilingDocsTool := WithMdoc, - scalacOptions in Tut --= Seq( - "-Xfatal-warnings", - "-Ywarn-unused-import", - "-Ywarn-numeric-widen", - "-Ywarn-dead-code", - "-Ywarn-unused:imports", - "-Xlint:-missing-interpolator,_" - ), micrositePushSiteWith := GitHub4s, micrositeGithubToken := sys.env.get("GITHUB_TOKEN"), micrositeExtraMdFiles := Map( @@ -85,11 +133,26 @@ lazy val site = project.in(file("site")) // General Settings lazy val commonSettings = Seq( - scalaVersion := "2.13.3", - crossScalaVersions := Seq(scalaVersion.value, "2.12.12"), + testFrameworks += new TestFramework("munit.Framework"), - addCompilerPlugin("org.typelevel" %% "kind-projector" % kindProjectorV cross CrossVersion.full), - addCompilerPlugin("com.olegpy" %% "better-monadic-for" % betterMonadicForV), + libraryDependencies ++= { + if (isDotty.value) Seq.empty + else Seq( + compilerPlugin("org.typelevel" % "kind-projector" % kindProjectorV cross CrossVersion.full), + compilerPlugin("com.olegpy" %% "better-monadic-for" % betterMonadicForV), + ) + }, + scalacOptions ++= { + if (isDotty.value) Seq("-source:3.0-migration") + else Seq() + }, + Compile / doc / sources := { + val old = (Compile / doc / sources).value + if (isDotty.value) + Seq() + else + old + }, libraryDependencies ++= Seq( "org.typelevel" %% "cats-core" % catsV, @@ -99,15 +162,18 @@ lazy val commonSettings = Seq( "co.fs2" %% "fs2-core" % fs2V, "co.fs2" %% "fs2-io" % fs2V, - "io.chrisdavenport" %% "keypool" % "0.2.0", + "org.typelevel" %% "keypool" % "0.3.0-RC1", + + "org.typelevel" %%% "munit-cats-effect-2" % munitCatsEffectV % Test, + "org.scalameta" %% "munit-scalacheck" % "0.7.20" % Test - "io.chrisdavenport" %% "log4cats-core" % log4catsV, - "io.chrisdavenport" %% "log4cats-slf4j" % log4catsV % Test, - "io.chrisdavenport" %% "log4cats-testing" % log4catsV % Test, + // "io.chrisdavenport" %% "log4cats-core" % log4catsV, + // "io.chrisdavenport" %% "log4cats-slf4j" % log4catsV % Test, + // "io.chrisdavenport" %% "log4cats-testing" % log4catsV % Test, - "org.specs2" %% "specs2-core" % specs2V % Test, - "org.specs2" %% "specs2-scalacheck" % specs2V % Test, - "com.codecommit" %% "cats-effect-testing-specs2" % "0.4.1" % Test + // "org.specs2" %% "specs2-core" % specs2V % Test, + // "org.specs2" %% "specs2-scalacheck" % specs2V % Test, + // "com.codecommit" %% "cats-effect-testing-specs2" % "0.4.1" % Test ) ) diff --git a/core/src/main/scala/io/chrisdavenport/rediculous/Redis.scala b/core/src/main/scala/io/chrisdavenport/rediculous/Redis.scala index 346028e..a3bec3a 100644 --- a/core/src/main/scala/io/chrisdavenport/rediculous/Redis.scala +++ b/core/src/main/scala/io/chrisdavenport/rediculous/Redis.scala @@ -54,7 +54,7 @@ object Redis { def apply[A](fa: Par[F,A]): Redis[F,A] = unwrap(fa) } - implicit def parApplicative[F[_]: Parallel: Bracket[*[_], Throwable]]: Applicative[Par[F, *]] = new Applicative[Par[F, *]]{ + implicit def parApplicative[F[_]: Parallel: BracketThrow]: Applicative[Par[F, *]] = new Applicative[Par[F, *]]{ def ap[A, B](ff: Par[F,A => B])(fa: Par[F,A]): Par[F,B] = Par(Redis( Par.unwrap(ff).unRedis.flatMap{ ff => Par.unwrap(fa).unRedis.map{fa => Parallel[F].sequential( diff --git a/core/src/main/scala/io/chrisdavenport/rediculous/RedisArg.scala b/core/src/main/scala/io/chrisdavenport/rediculous/RedisArg.scala index bfcb28e..c3be3e3 100644 --- a/core/src/main/scala/io/chrisdavenport/rediculous/RedisArg.scala +++ b/core/src/main/scala/io/chrisdavenport/rediculous/RedisArg.scala @@ -15,20 +15,20 @@ object RedisArg { } } - implicit val string = new RedisArg[String] { + implicit val string : RedisArg[String] = new RedisArg[String] { def encode(a: String): String = a } - implicit val int = new RedisArg[Int] { + implicit val int: RedisArg[Int] = new RedisArg[Int] { def encode(a: Int): String = a.toString() } - implicit val long = new RedisArg[Long] { + implicit val long: RedisArg[Long] = new RedisArg[Long] { def encode(a: Long): String = a.toString() } // Check this later - implicit val double = new RedisArg[Double] { + implicit val double: RedisArg[Double] = new RedisArg[Double] { def encode(a: Double): String = if (a.isInfinite() && a > 0) "+inf" else if (a.isInfinite() && a < 0) "-inf" diff --git a/core/src/main/scala/io/chrisdavenport/rediculous/RedisConnection.scala b/core/src/main/scala/io/chrisdavenport/rediculous/RedisConnection.scala index 8dffd21..13fe343 100644 --- a/core/src/main/scala/io/chrisdavenport/rediculous/RedisConnection.scala +++ b/core/src/main/scala/io/chrisdavenport/rediculous/RedisConnection.scala @@ -1,12 +1,12 @@ package io.chrisdavenport.rediculous -import cats.effect._ +import cats.effect.{MonadThrow => _, _} import cats.effect.concurrent._ import cats.effect.implicits._ import cats._ import cats.implicits._ import cats.data._ -import io.chrisdavenport.keypool._ +import _root_.org.typelevel.keypool._ import fs2.concurrent.Queue import fs2.io.tcp._ import fs2._ @@ -30,7 +30,7 @@ object RedisConnection{ // Guarantees With Socket That Each Call Receives a Response // Chunk must be non-empty but to do so incurs a penalty - private[rediculous] def explicitPipelineRequest[F[_]: MonadError[*[_], Throwable]](socket: Socket[F], calls: Chunk[Resp], maxBytes: Int = 8 * 1024 * 1024, timeout: Option[FiniteDuration] = 5.seconds.some): F[List[Resp]] = { + private[rediculous] def explicitPipelineRequest[F[_]: MonadThrow](socket: Socket[F], calls: Chunk[Resp], maxBytes: Int = 8 * 1024 * 1024, timeout: Option[FiniteDuration] = 5.seconds.some): F[List[Resp]] = { def getTillEqualSize(acc: List[List[Resp]], lastArr: Array[Byte]): F[List[Resp]] = socket.read(maxBytes, timeout).flatMap{ case None => @@ -87,7 +87,7 @@ object RedisConnection{ def runRequest[F[_]: Concurrent, A: RedisResult](connection: RedisConnection[F])(input: NonEmptyList[String], key: Option[String]): F[F[Either[Resp, A]]] = runRequestInternal(connection)(NonEmptyList.of(input), key).map(_.map(nel => RedisResult[A].decode(nel.head))) - def runRequestTotal[F[_]: Concurrent, A: RedisResult](input: NonEmptyList[String], key: Option[String]): Redis[F, A] = Redis(Kleisli{connection: RedisConnection[F] => + def runRequestTotal[F[_]: Concurrent, A: RedisResult](input: NonEmptyList[String], key: Option[String]): Redis[F, A] = Redis(Kleisli{(connection: RedisConnection[F]) => runRequest(connection)(input, key).map{ fE => fE.flatMap{ case Right(a) => a.pure[F] @@ -97,7 +97,7 @@ object RedisConnection{ } }) - private[rediculous] def closeReturn[F[_]: MonadError[*[_], Throwable], A](fE: F[Either[Resp, A]]): F[A] = + private[rediculous] def closeReturn[F[_]: MonadThrow, A](fE: F[Either[Resp, A]]): F[A] = fE.flatMap{ case Right(a) => a.pure[F] case Left(e@Resp.Error(_)) => ApplicativeError[F, Throwable].raiseError[A](e) @@ -191,7 +191,7 @@ object RedisConnection{ ): Resource[F, RedisConnection[F]] = for { keypool <- KeyPoolBuilder[F, (String, Int), (Socket[F], F[Unit])]( - {t: (String, Int) => sg.client[F](new InetSocketAddress(t._1, t._2)) + {(t: (String, Int)) => sg.client[F](new InetSocketAddress(t._1, t._2)) .flatMap(elevateSocket(_, tlsContext, tlsParameters)) .allocated }, @@ -319,7 +319,7 @@ object RedisConnection{ Concurrent[F].start(fa.flatMap(a => deferred.complete(a).as(a)).attempt) ) ){ - fibers: NonEmptyList[Fiber[F, Either[Throwable, A]]] => + (fibers: NonEmptyList[Fiber[F, Either[Throwable, A]]]) => Concurrent[F].race( fibers.traverse(_.join).map( _.traverse(_.swap).swap diff --git a/core/src/main/scala/io/chrisdavenport/rediculous/RedisPipeline.scala b/core/src/main/scala/io/chrisdavenport/rediculous/RedisPipeline.scala index ec19695..dd3ddac 100644 --- a/core/src/main/scala/io/chrisdavenport/rediculous/RedisPipeline.scala +++ b/core/src/main/scala/io/chrisdavenport/rediculous/RedisPipeline.scala @@ -23,12 +23,14 @@ object RedisPipeline { implicit val ctx: RedisCtx[RedisPipeline] = new RedisCtx[RedisPipeline]{ def keyed[A: RedisResult](key: String, command: NonEmptyList[String]): RedisPipeline[A] = RedisPipeline(RedisTransaction.RedisTxState{for { - (i, base, value) <- State.get + s1 <- State.get[(Int, List[NonEmptyList[String]], Option[String])] + (i, base, value) = s1 _ <- State.set((i + 1, command :: base, value.orElse(Some(key)))) } yield RedisTransaction.Queued(l => RedisResult[A].decode(l(i)))}) def unkeyed[A: RedisResult](command: NonEmptyList[String]): RedisPipeline[A] = RedisPipeline(RedisTransaction.RedisTxState{for { - (i, base, value) <- State.get + out <- State.get[(Int, List[NonEmptyList[String]], Option[String])] + (i, base, value) = out _ <- State.set((i + 1, command :: base, value)) } yield RedisTransaction.Queued(l => RedisResult[A].decode(l(i)))}) } @@ -55,7 +57,7 @@ object RedisPipeline { class SendPipelinePartiallyApplied[F[_]]{ def apply[A](tx: RedisPipeline[A])(implicit F: Concurrent[F]): Redis[F, A] = { - Redis(Kleisli{c: RedisConnection[F] => + Redis(Kleisli{(c: RedisConnection[F]) => val ((_, commandsR, key), RedisTransaction.Queued(f)) = tx.value.value.run((0, List.empty, None)).value val commands = commandsR.reverse.toNel commands.traverse(nelCommands => RedisConnection.runRequestInternal(c)(nelCommands, key) // We Have to Actually Send A Command diff --git a/core/src/main/scala/io/chrisdavenport/rediculous/RedisResult.scala b/core/src/main/scala/io/chrisdavenport/rediculous/RedisResult.scala index 8f0100f..46980de 100644 --- a/core/src/main/scala/io/chrisdavenport/rediculous/RedisResult.scala +++ b/core/src/main/scala/io/chrisdavenport/rediculous/RedisResult.scala @@ -46,7 +46,7 @@ object RedisResult extends RedisResultLowPriority{ } } - implicit val redisType = new RedisResult[RedisProtocol.RedisType] { + implicit val redisType: RedisResult[RedisProtocol.RedisType] = new RedisResult[RedisProtocol.RedisType] { def decode(resp: Resp): Either[Resp,RedisProtocol.RedisType] = resp match { case Resp.SimpleString(value) => Either.right(value match { case "none" => RedisProtocol.RedisType.None diff --git a/core/src/main/scala/io/chrisdavenport/rediculous/RedisTransaction.scala b/core/src/main/scala/io/chrisdavenport/rediculous/RedisTransaction.scala index 24de9d1..2f74180 100644 --- a/core/src/main/scala/io/chrisdavenport/rediculous/RedisTransaction.scala +++ b/core/src/main/scala/io/chrisdavenport/rediculous/RedisTransaction.scala @@ -44,12 +44,14 @@ object RedisTransaction { implicit val ctx: RedisCtx[RedisTransaction] = new RedisCtx[RedisTransaction]{ def keyed[A: RedisResult](key: String, command: NonEmptyList[String]): RedisTransaction[A] = RedisTransaction(RedisTxState{for { - (i, base, value) <- State.get + out <- State.get[(Int, List[NonEmptyList[String]], Option[String])] + (i, base, value) = out _ <- State.set((i + 1, command :: base, value.orElse(Some(key)))) } yield Queued(l => RedisResult[A].decode(l(i)))}) def unkeyed[A: RedisResult](command: NonEmptyList[String]): RedisTransaction[A] = RedisTransaction(RedisTxState{for { - (i, base, value) <- State.get + out <- State.get[(Int, List[NonEmptyList[String]], Option[String])] + (i, base, value) = out _ <- State.set((i + 1, command :: base, value)) } yield Queued(l => RedisResult[A].decode(l(i)))}) } @@ -72,7 +74,7 @@ object RedisTransaction { sealed trait TxResult[+A] object TxResult { final case class Success[A](value: A) extends TxResult[A] - final case object Aborted extends TxResult[Nothing] + case object Aborted extends TxResult[Nothing] final case class Error(value: String) extends TxResult[Nothing] } @@ -115,7 +117,7 @@ object RedisTransaction { class MultiExecPartiallyApplied[F[_]]{ def apply[A](tx: RedisTransaction[A])(implicit F: Concurrent[F]): Redis[F, TxResult[A]] = { - Redis(Kleisli{c: RedisConnection[F] => + Redis(Kleisli{(c: RedisConnection[F]) => val ((_, commandsR, key), Queued(f)) = tx.value.value.run((0, List.empty, None)).value val commands = commandsR.reverse RedisConnection.runRequestInternal(c)(NonEmptyList( diff --git a/core/src/main/scala/io/chrisdavenport/rediculous/Resp.scala b/core/src/main/scala/io/chrisdavenport/rediculous/Resp.scala index cf31cc1..04d76cc 100644 --- a/core/src/main/scala/io/chrisdavenport/rediculous/Resp.scala +++ b/core/src/main/scala/io/chrisdavenport/rediculous/Resp.scala @@ -10,7 +10,12 @@ sealed trait Resp object Resp { import scala.{Array => SArray} - sealed trait RespParserResult[+A] + sealed trait RespParserResult[+A]{ + def extract: Option[A] = this match { + case ParseComplete(out, _) => Some(out) + case _ => None + } + } final case class ParseComplete[A](value: A, rest: SArray[Byte]) extends RespParserResult[A] final case class ParseIncomplete(arr: SArray[Byte]) extends RespParserResult[Nothing] final case class ParseError(message: String, cause: Option[Throwable]) extends RedisError with RespParserResult[Nothing] diff --git a/core/src/main/scala/io/chrisdavenport/rediculous/cluster/ClusterCommands.scala b/core/src/main/scala/io/chrisdavenport/rediculous/cluster/ClusterCommands.scala index c49f48b..627ce01 100644 --- a/core/src/main/scala/io/chrisdavenport/rediculous/cluster/ClusterCommands.scala +++ b/core/src/main/scala/io/chrisdavenport/rediculous/cluster/ClusterCommands.scala @@ -9,7 +9,7 @@ object ClusterCommands { final case class ClusterServer(host: String, port: Int, id: String) object ClusterServer { - implicit val result = new RedisResult[ClusterServer]{ + implicit val result: RedisResult[ClusterServer] = new RedisResult[ClusterServer]{ def decode(resp: Resp): Either[Resp,ClusterServer] = resp match { case Resp.Array(Some(Resp.BulkString(Some(host)) :: Resp.Integer(l) :: Resp.BulkString(Some(id)) :: Nil)) => ClusterServer(host, l.toInt, id).asRight @@ -20,7 +20,7 @@ object ClusterCommands { final case class ClusterSlot(start: Int, end: Int, replicas: List[ClusterServer]) object ClusterSlot { - implicit val result = new RedisResult[ClusterSlot]{ + implicit val result: RedisResult[ClusterSlot] = new RedisResult[ClusterSlot]{ def decode(resp: Resp): Either[Resp,ClusterSlot] = resp match { case Resp.Array(Some(Resp.Integer(start) :: Resp.Integer(end) :: rest)) => rest.traverse(RedisResult[ClusterServer].decode).map{l => @@ -48,7 +48,7 @@ object ClusterCommands { } } object ClusterSlots { - implicit val result = new RedisResult[ClusterSlots]{ + implicit val result: RedisResult[ClusterSlots] = new RedisResult[ClusterSlots]{ def decode(resp: Resp): Either[Resp,ClusterSlots] = RedisResult[List[ClusterSlot]].decode(resp).map(ClusterSlots(_)) } diff --git a/core/src/test/scala/io/chrisdavenport/rediculous/RespSpec.scala b/core/src/test/scala/io/chrisdavenport/rediculous/RespSpec.scala index 1c16c26..4e10729 100644 --- a/core/src/test/scala/io/chrisdavenport/rediculous/RespSpec.scala +++ b/core/src/test/scala/io/chrisdavenport/rediculous/RespSpec.scala @@ -1,186 +1,200 @@ package io.chrisdavenport.rediculous -import org.specs2.mutable.Specification -import org.specs2.ScalaCheck +import cats.syntax.all._ import io.chrisdavenport.rediculous.Resp.ParseComplete -import io.chrisdavenport.rediculous.Resp.ParseIncomplete -import io.chrisdavenport.rediculous.Resp.ParseError +import org.scalacheck.Prop -class RespSpec extends Specification with ScalaCheck { - "Resp" should { - "parse a simple-string" in { +class RespSpec extends munit.ScalaCheckSuite { + // "Resp" should { + test("Resp parse a simple-string") { Resp.SimpleString.parse("+OK\r\n".getBytes()) match { case ParseComplete(value, _) => - value.value must_=== "OK" - case _ => ko + assert(value.value === "OK") + case _ => fail("Did not complete") } } - "round-trip a simple-string" >> prop { s: String => (!s.contains("\r") && !s.contains("\n")) ==> { - // val initS = s.replace("\r", "").replace("\n", "") // simple strings cannot contain new lines - val init = Resp.SimpleString(s) - Resp.SimpleString.parse( - Resp.SimpleString.encode(init) - ) match { - case ParseComplete(value, rest) => - (value must_=== init).and( - rest must beEmpty - ) - case o => ko(s"Unexpected: $o") - } - }} - "parse an error" in { + // Figure out how to constrain + // property("round-trip a simple-string"){ Prop.forAll { (s: String) => (!s.contains("\r") && !s.contains("\n")) ==> { + // // val initS = s.replace("\r", "").replace("\n", "") // simple strings cannot contain new lines + // val init = Resp.SimpleString(s) + // Resp.SimpleString.parse( + // Resp.SimpleString.encode(init) + // ) match { + // case ParseComplete(value, rest) => + // (value must_=== init).and( + // rest must beEmpty + // ) + // case o => ko(s"Unexpected: $o") + // } + // }}} + + test("Resp parse an error"){ val s = "-Error message\r\n" Resp.Error.parse(s.getBytes()) match { case ParseComplete(value, _) => - value.value must_=== "Error message" - case o => ko(s"Unexpected: $o") + assert(value.value === "Error message") + case o => fail(s"Unexpected: $o") } } - "round-trip an error" in prop{s : String => (!s.contains("\r") && !s.contains("\n")) ==> { - val init = Resp.Error(s) - Resp.Error.parse( - Resp.Error.encode(init) - ) match { - case ParseComplete(value, rest) => - (value.value must_=== init.value).and( - rest must beEmpty - ) - case o => ko(s"Unexpected $o") - } - }} - - "parse an integer" in { + // Figure out how to constraint + // property("round-trip an error"){Prop.forAll{(s : String) => (!s.contains("\r") && !s.contains("\n")) ==> { + // val init = Resp.Error(s) + // Resp.Error.parse( + // Resp.Error.encode(init) + // ) match { + // case ParseComplete(value, rest) => + // assert(value.value === init.value && rest.isEmpty) + // case o => fail(s"Unexpected $o") + // } + // }}} + + test("Resp parse an integer") { val s = ":1000\r\n" Resp.Integer.parse(s.getBytes()) match { case ParseComplete(value, _) => - value.long must_=== 1000L - case o => ko(s"Unexpected $o") + assert(value.long === 1000L) + case o => fail(s"Unexpected $o") } } - "encode an integer" in { + test("Resp encode an integer") { val x = 8971392965300387418L val arr = Resp.Integer.encode(Resp.Integer(x)) val s = new String(arr) - s must_=== ":8971392965300387418\r\n" + assert(s === ":8971392965300387418\r\n") } - "round-trip an integer" >> prop {l: Long => - val init = Resp.Integer(l) - Resp.Integer.parse( - Resp.Integer.encode(init) - ) match { - case ParseComplete(value, rest) => - (value must_=== init).and( - rest must beEmpty - ) - case o => ko(s"Got Unexpected Result $o") + property("Resp round-trip an integer"){ + Prop.forAll{ (l: Long) => + val init = Resp.Integer(l) + Resp.Integer.parse( + Resp.Integer.encode(init) + ).extract match { + case Some(value) => + assertEquals(value, init) + case o => fail(s"Got Unexpected Result $o") + } } } - "parse a bulk string" in { + test("Resp parse a bulk string") { val s = "$6\r\nfoobar\r\n" Resp.BulkString.parse(s.getBytes()) match { case ParseComplete(value, _) => - value.value must beSome.like{ case s2 => s2 must_=== "foobar"} - case o => ko(s"Unexpected $o") + value.value match { + case Some(s2) => assert(s2 === "foobar") + case None => fail("None") + } + case o => fail(s"Unexpected $o") } } - "parse an empty bulk string" in { + test("Resp parse an empty bulk string") { val s = "$0\r\n\r\n" Resp.BulkString.parse(s.getBytes()) match { case ParseComplete(value, _) => - value.value must beSome.like{ case s2 => s2 must_=== ""} - case o => ko(s"Unexpected $o") + value.value match { + case Some(s2) => assert(s2 === "") + case None => fail("Empty Value") + } + case o => fail(s"Unexpected $o") } } - "parse a null bulk string" in { + test("Resp parse a null bulk string") { val s = "$-1\r\n" Resp.BulkString.parse(s.getBytes()) match { case ParseComplete(value, _) => - value.value must beNone - case o => ko(s"Unexpected $o") + assert(value.value === None) + case o => fail(s"Unexpected $o") } - } - "round-trip a bulk-string" >> prop{ s : String => + property("Resp round-trip a bulk-string"){ Prop.forAll{ (s : String) => val init = Resp.BulkString(Some(s)) Resp.BulkString.parse( Resp.BulkString.encode(init) - ) match { - case ParseComplete(value, rest) => - (value must_=== init).and( - rest must beEmpty - ) - case p@ParseIncomplete(_) => ko(s"Got Incomplete Result $p") - case e@ParseError(_,_) => ko(s"Got ParseError $e") - } + ).extract match { + case Some(value) => + assertEquals(value, init) + + case None => fail(s"failed with input $s") + }} } - "parse an empty array" in { + test("Resp parse an empty array") { val init = "*0\r\n" Resp.Array.parse(init.getBytes()) match { case ParseComplete(value, _) => - value.a must beSome.like{ case l => l must beEmpty} - case o => ko(s"Unexpected $o") + value.a match { + case Some(l) => assert (l.isEmpty) + case None => fail("Array Returned None") + } + case o => fail(s"Unexpected $o") } } - "parse an array of bulk strings" in { + test("Resp parse an array of bulk strings") { val init = "*2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n" Resp.Array.parse(init.getBytes()) match { case ParseComplete(value, _) => - value.a must beSome.like{ case l => l must_=== List(Resp.BulkString(Some("foo")), Resp.BulkString(Some("bar")))} - case o => ko(s"Unexpected $o") + value.a match { + case Some(Resp.BulkString(Some("foo")) :: Resp.BulkString(Some("bar")) :: Nil) => assert(true) + case otherwise => fail(s"Incorrect Output $otherwise") + } + case o => fail(s"Unexpected $o") } } - "parse an array of integers" in { + test("Resp parse an array of integers") { val init = "*3\r\n:1\r\n:2\r\n:3\r\n" Resp.Array.parse(init.getBytes()) match { case ParseComplete(value, _) => - value.a must beSome.like{ case l => - l must_=== List(Resp.Integer(1L), Resp.Integer(2L), Resp.Integer(3L))} - case o => ko(s"Unexpected $o") + value.a match { + case Some(l) => + assertEquals(l, List(Resp.Integer(1L), Resp.Integer(2L), Resp.Integer(3L))) + case None => fail("Incorrect Value") + } + case o => fail(s"Unexpected $o") } } - "parse an array of mixed types" in { + test("Resp parse an array of mixed types") { val init = "*5\r\n:1\r\n:2\r\n:3\r\n:4\r\n$6\r\nfoobar\r\n" Resp.Array.parse(init.getBytes()) match { case ParseComplete(value, _) => - value.a must beSome.like{ case l => - l must_=== List(Resp.Integer(1L), Resp.Integer(2L), Resp.Integer(3L), Resp.Integer(4L), Resp.BulkString(Some("foobar")))} - case o => ko(s"Unexpected $o") + value.a match { + case Some(l) => + assertEquals(l, List(Resp.Integer(1L), Resp.Integer(2L), Resp.Integer(3L), Resp.Integer(4L), Resp.BulkString(Some("foobar")))) + case None => fail("Value Empty") + } + case o => fail(s"Unexpected $o") } } - "parse a null array" in { + test("Resp parse a null array") { val init = "*-1\r\n" Resp.Array.parse(init.getBytes()) match { case ParseComplete(value, _) => - value.a must beNone - case o => ko(s"Unexpected $o") + assertEquals(value.a, None) + case o => fail(s"Unexpected $o") } } - "parse an array of arrays" in { + test("Resp parse an array of arrays") { val init = "*2\r\n*3\r\n:1\r\n:2\r\n:3\r\n*2\r\n+Foo\r\n-Bar\r\n" - Resp.Array.parse(init.getBytes()) match { - case ParseComplete(value, _) => - value must_=== Resp.Array(Some( + Resp.Array.parse(init.getBytes()).extract match { + case Some(value) => + val expected = Resp.Array(Some( List( Resp.Array(Some(List(Resp.Integer(1), Resp.Integer(2), Resp.Integer(3)))), Resp.Array(Some(List(Resp.SimpleString("Foo"), Resp.Error("Bar")))) ) )) - case o => ko(s"Unexpected $o") + assertEquals(value, expected) + case o => fail(s"Unexpected $o") } } - } } \ No newline at end of file diff --git a/core/src/test/scala/io/chrisdavenport/rediculous/cluster/CRC16Spec.scala b/core/src/test/scala/io/chrisdavenport/rediculous/cluster/CRC16Spec.scala index a228030..251b8fc 100644 --- a/core/src/test/scala/io/chrisdavenport/rediculous/cluster/CRC16Spec.scala +++ b/core/src/test/scala/io/chrisdavenport/rediculous/cluster/CRC16Spec.scala @@ -1,11 +1,11 @@ package io.chrisdavenport.rediculous.cluster -class CRC16Spec extends org.specs2.mutable.Specification { - "CRC16" should { - "outputs the right hash for known input" in { - val out = CRC16.string("123456789") - val hex = Integer.toHexString(out) - hex must_=== "31c3" - } +import cats.syntax.all._ + +class CRC16Spec extends munit.FunSuite { + test("CRC16 outputs the right hash for known input") { + val out = CRC16.string("123456789") + val hex = Integer.toHexString(out) + assert(hex === "31c3") } } \ No newline at end of file diff --git a/core/src/test/scala/io/chrisdavenport/rediculous/cluster/HashSlotSpec.scala b/core/src/test/scala/io/chrisdavenport/rediculous/cluster/HashSlotSpec.scala index 289b170..1030d95 100644 --- a/core/src/test/scala/io/chrisdavenport/rediculous/cluster/HashSlotSpec.scala +++ b/core/src/test/scala/io/chrisdavenport/rediculous/cluster/HashSlotSpec.scala @@ -1,30 +1,28 @@ package io.chrisdavenport.rediculous.cluster -import cats.effect.testing.specs2.CatsIO +import cats.syntax.all._ -class HashSlotSpec extends org.specs2.mutable.Specification with org.specs2.ScalaCheck with CatsIO { - "HashSlot.hashKey" should { - "Find the right key section for a keyslot" in { +class HashSlotSpec extends munit.FunSuite { + test("HashSlot.hashKey Find the right key section for a keyslot"){ val input = "{user.name}.foo" - HashSlot.hashKey(input) must_=== "user.name" + assert(HashSlot.hashKey(input) === "user.name") } - "Find the right key in middle of key" in { + test("HashSlot.hashKey Find the right key in middle of key") { val input = "bar{foo}baz" - HashSlot.hashKey(input) must_=== "foo" + assert(HashSlot.hashKey(input) === "foo") } - "Find the right key at end of key" in { + test("HashSlot.hashKey Find the right key at end of key"){ val input = "barbaz{foo}" - HashSlot.hashKey(input) must_=== "foo" + assert(HashSlot.hashKey(input) === "foo") } - "output original key if braces are directly next to each other" in { + test("HashSlot.hashKey output original key if braces are directly next to each other"){ val input = "{}.bar" - HashSlot.hashKey(input) must_=== input + assert(HashSlot.hashKey(input) === input) } - "output the full value if no keyslot present" in { + test("HashSlot.hashKey output the full value if no keyslot present") { val input = "bazbarfoo" - HashSlot.hashKey(input) must_=== input + assert(HashSlot.hashKey(input) === input) } - } /** import cats.implicits._ @@ -48,7 +46,6 @@ class HashSlotSpec extends org.specs2.mutable.Specification with org.specs2.Scal out => HashSlot.find(s) must_=== out } } - } **/ } \ No newline at end of file diff --git a/project/build.properties b/project/build.properties index 0837f7a..d91c272 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.3.13 +sbt.version=1.4.6 diff --git a/project/plugins.sbt b/project/plugins.sbt index 6b8dc1e..6535792 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,11 +1,14 @@ -addSbtPlugin("io.github.davidgregory084" % "sbt-tpolecat" % "0.1.13") +addSbtPlugin("io.github.davidgregory084" % "sbt-tpolecat" % "0.1.16") -addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "0.6.1") -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.33") - -addSbtPlugin("com.geirsson" % "sbt-ci-release" % "1.5.3") +addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.0.0") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.4.0") +addSbtPlugin("com.github.cb372" % "sbt-explicit-dependencies" % "0.2.16") +addSbtPlugin("com.geirsson" % "sbt-ci-release" % "1.5.5") addSbtPlugin("io.chrisdavenport" % "sbt-mima-version-check" % "0.1.2") addSbtPlugin("io.chrisdavenport" % "sbt-no-publish" % "0.1.0") +addSbtPlugin("ch.epfl.lamp" % "sbt-dotty" % "0.5.1") +addSbtPlugin("com.codecommit" % "sbt-github-actions" % "0.9.5") -addSbtPlugin("com.47deg" % "sbt-microsites" % "1.1.5") -addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.2.3") \ No newline at end of file +addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.2.14") +addSbtPlugin("com.47deg" % "sbt-microsites" % "1.3.0") +addSbtPlugin("com.typesafe.sbt" % "sbt-ghpages" % "0.6.3") \ No newline at end of file