Skip to content

Commit

Permalink
Support for Scala 3 (#287)
Browse files Browse the repository at this point in the history
  • Loading branch information
KarelCemus committed Feb 18, 2024
1 parent 606c5c7 commit 6a395b2
Show file tree
Hide file tree
Showing 25 changed files with 424 additions and 73 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-22.04
timeout-minutes: 15
env:
SBT_OPTS: -Dfile.encoding=UTF-8 -Duser.timezone=UTC
SBT_OPTS: "-Dfile.encoding=UTF-8 -Duser.timezone=UTC -Xmx2g"
steps:
- name: Checkout code
uses: actions/checkout@v4
Expand All @@ -31,8 +31,8 @@ jobs:
jvm: adoptium:1.11

- name: Build
timeout-minutes: 10
timeout-minutes: 15
env:
COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
sbt --client "+clean; +compile; +Test/compile; lint; +coverage; +test; +coverageReport; +coveralls;"
sbt -v "+clean; +compile; +Test/compile; lint; +coverage; +test; +coverageReport; +coveralls;"
6 changes: 3 additions & 3 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
timeout-minutes: 15
environment: "Generally Available"
env:
SBT_OPTS: -Dfile.encoding=UTF-8 -Duser.timezone=UTC
SBT_OPTS: "-Dfile.encoding=UTF-8 -Duser.timezone=UTC -Xmx2g"
steps:
- name: Checkout code
uses: actions/checkout@v4
Expand All @@ -36,10 +36,10 @@ jobs:
echo $PGP_SECRET | base64 --decode | gpg --batch --import
- name: Test and Publish JARs
timeout-minutes: 10
timeout-minutes: 15
env:
PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }}
SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }}
run: |
sbt --client "+test; +publishSigned; sonatypeBundleRelease;"
sbt -v "+test; +publishSigned; sonatypeBundleRelease;"
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
Migration to Pekko and Play 3.0, thanks to @TomJKing for help in [#272](https://github.com/KarelCemus/play-redis/pull/272),
finished in [#278](https://github.com/KarelCemus/play-redis/pull/278)

Added support to Scala 3 in [#264](https://github.com/KarelCemus/play-redis/issues/264)

### [:link: 3.0.0](https://github.com/KarelCemus/play-redis/tree/3.0.0-M1)

Updated to Play `2.9.0` and dropped `Scala 2.12` since it was discontinued from the Play framework.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# Redis Cache module for Play framework

**This version supports Play framework 3.0.x with JDK 11 and Scala 2.13. Scala 3 on the roadmap.**<br/>
**This version supports Play framework 3.0.x with JDK 11 and Scala 2.13 and Scala 3.**<br/>
**For previous versions see older releases.**

[![Travis CI: Status](https://travis-ci.org/KarelCemus/play-redis.svg?branch=master)](https://travis-ci.org/KarelCemus/play-redis)
Expand Down
16 changes: 10 additions & 6 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,20 @@ description := "Redis cache plugin for the Play framework 2"

organization := "com.github.karelcemus"

crossScalaVersions := Seq("2.13.12") //, "3.3.0"
crossScalaVersions := Seq("2.13.12", "3.3.1")

scalaVersion := crossScalaVersions.value.head

playVersion := "3.0.0"
playVersion := "3.0.1"

libraryDependencies ++= Seq(
// play framework cache API
"org.playframework" %% "play-cache" % playVersion.value % Provided,
// redis connector
"io.github.rediscala" %% "rediscala" % "1.14.0-pekko",
// test framework with mockito extension
"org.scalatest" %% "scalatest" % "3.2.17" % Test,
"org.scalamock" %% "scalamock" % "5.2.0" % Test,
"org.scalatest" %% "scalatest" % "3.2.18" % Test,
"org.scalamock" %% "scalamock" % "6.0.0-M1" % Test,
// test module for play framework
"org.playframework" %% "play-test" % playVersion.value % Test,
// to run integration tests
Expand All @@ -37,7 +37,11 @@ resolvers ++= Seq(

javacOptions ++= Seq("-Xlint:unchecked", "-encoding", "UTF-8")

scalacOptions ++= Seq("-deprecation", "-feature", "-unchecked", "-Ywarn-unused")
scalacOptions ++= Seq("-deprecation", "-feature", "-unchecked")

scalacOptions ++= {
if (scalaVersion.value.startsWith("2.")) Seq("-Ywarn-unused") else Seq.empty
}

enablePlugins(CustomReleasePlugin)

Expand All @@ -47,7 +51,6 @@ coverageExcludedFiles := ".*exceptions.*"
Test / test := (Test / testOnly).toTask(" * -- -l \"org.scalatest.Ignore\"").value

semanticdbEnabled := true
semanticdbOptions += "-P:semanticdb:synthetics:on"
semanticdbVersion := scalafixSemanticdb.revision
ThisBuild / scalafixScalaBinaryVersion := CrossVersion.binaryScalaVersion(scalaVersion.value)

Expand All @@ -57,6 +60,7 @@ wartremoverWarnings ++= Warts.allBut(
Wart.AsInstanceOf,
Wart.AutoUnboxing,
Wart.DefaultArguments,
Wart.FinalVal,
Wart.GlobalExecutionContext,
Wart.ImplicitConversion,
Wart.ImplicitParameter,
Expand Down
File renamed without changes.
31 changes: 31 additions & 0 deletions src/main/scala-3/play/api/cache/redis/impl/RedisSetJavaImpl.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package play.api.cache.redis.impl

import play.api.cache.redis.RedisSet
import play.cache.redis.AsyncRedisSet

import scala.concurrent.Future

class RedisSetJavaImpl[Elem](internal: RedisSet[Elem, Future])(implicit runtime: RedisRuntime) extends AsyncRedisSet[Elem] {
import JavaCompatibility.*

override def add(elements: Array[? <: Elem]): CompletionStage[AsyncRedisSet[Elem]] =
async { implicit context =>
internal.add(elements.toSeq: _*).map(_ => this)
}

override def contains(element: Elem): CompletionStage[java.lang.Boolean] =
async { implicit context =>
internal.contains(element).map(Boolean.box)
}

override def remove(elements: Array[? <: Elem]): CompletionStage[AsyncRedisSet[Elem]] =
async { implicit context =>
internal.remove(elements.toSeq: _*).map(_ => this)
}

override def toSet: CompletionStage[JavaSet[Elem]] =
async { implicit context =>
internal.toSet.map(_.asJava)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ private[connector] class PekkoDecoder(serializer: Serialization) {
Base64.getDecoder.decode(base64)

/** deserializes the binary stream into the object */
@SuppressWarnings(Array("org.wartremover.warts.RedundantAsInstanceOf"))
private def binaryToAnyRef[T](binary: Array[Byte])(implicit classTag: ClassTag[T]): AnyRef =
serializer.deserialize(binary, classTag.runtimeClass.asInstanceOf[Class[? <: AnyRef]]).get

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ private[connector] class RedisConnectorImpl(serializer: PekkoSerializer, redis:
case length =>
log.debug(s"Inserted $value into the list at '$key'. New size is $length.")
Some(length)
} recover [Option[Long]] {
} recover {
case ExecutionFailedException(_, _, _, ex) if ex.getMessage startsWith "WRONGTYPE" =>
log.warn(s"Value at '$key' is not a list.")
throw new IllegalArgumentException(s"Value at '$key' is not a list.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ sealed trait InvocationPolicy {
object EagerInvocation extends InvocationPolicy {

override def invoke[T](f: => Future[Any], thenReturn: T)(implicit context: ExecutionContext): Future[T] = {
f: Unit
val _ = f
Future successful thenReturn
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ private[impl] object JavaCompatibility extends JavaCompatibilityBase {

def apply[T](values: T*): JavaList[T] = {
val list = new java.util.ArrayList[T]()
list.addAll(values.asJava): Unit
val _ = list.addAll(values.asJava)
list
}

Expand Down
4 changes: 2 additions & 2 deletions src/main/scala/play/api/cache/redis/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ package object redis extends AnyRef with ExpirationImplicits with ExceptionImpli
}

@SuppressWarnings(Array("org.wartremover.warts.Equals"))
implicit final class HigherKindedAnyOps[F[_]](private val self: F[?]) extends AnyVal {
def =~=(other: F[?]): Boolean = self == other
implicit final class HigherKindedAnyOps[F[_], A](private val self: F[A]) extends AnyVal {
def =~=[T](other: F[T]): Boolean = self == other
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package play.api.cache.redis.connector

import org.scalamock.scalatest.AsyncMockFactory
import redis.{ByteStringSerializer, RedisCommands}

import scala.concurrent.Future

private trait RedisCommandsMock extends RedisCommands {

final override def zadd[V: ByteStringSerializer](key: String, scoreMembers: (Double, V)*): Future[Long] =
zaddMock(key, scoreMembers)

def zaddMock[V: ByteStringSerializer](key: String, scoreMembers: Seq[(Double, V)]): Future[Long]

final override def zrem[V: ByteStringSerializer](key: String, members: V*): Future[Long] =
zremMock(key, members)

def zremMock[V: ByteStringSerializer](key: String, members: Seq[V]): Future[Long]
}

private object RedisCommandsMock {

def mock(factory: AsyncMockFactory): (RedisCommands, RedisCommandsMock) = {
val mock = factory.mock[RedisCommandsMock](factory)
(mock, mock)
}

}
28 changes: 28 additions & 0 deletions src/test/scala-2.13/play/api/cache/redis/impl/AsyncRedisMock.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package play.api.cache.redis.impl

import org.scalamock.scalatest.AsyncMockFactory
import play.api.cache.redis.{AsynchronousResult, Done}

import scala.reflect.ClassTag

private trait AsyncRedisMock extends AsyncRedis {

final override def removeAll(keys: String*): AsynchronousResult[Done] =
removeAllKeys(keys)

def removeAllKeys(keys: Seq[String]): AsynchronousResult[Done]

final override def getAll[T: ClassTag](keys: Iterable[String]): AsynchronousResult[Seq[Option[T]]] =
getAllKeys(keys)

def getAllKeys[T](keys: Iterable[String]): AsynchronousResult[Seq[Option[T]]]
}

private object AsyncRedisMock {

def mock(factory: AsyncMockFactory): (AsyncRedis, AsyncRedisMock) = {
val mock = factory.mock[AsyncRedisMock](factory)
(mock, mock)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package play.api.cache.redis.connector

import org.scalamock.scalatest.AsyncMockFactory
import redis.*
import redis.api.ListPivot
import redis.protocol.RedisReply

import scala.concurrent.{ExecutionContext, Future}

// this is implemented due to a bug in ScalaMock 6.0.0-M1 and reported as https://github.com/paulbutcher/ScalaMock/issues/503
private trait RedisCommandsMock {

def get[R: ByteStringDeserializer](key: String): Future[Option[R]]

def set[V: ByteStringSerializer](key: String, value: V, exSeconds: Option[Long], pxMilliseconds: Option[Long], NX: Boolean, XX: Boolean): Future[Boolean]

def expire(key: String, seconds: Long): Future[Boolean]

def mset[V: ByteStringSerializer](keysValues: Map[String, V]): Future[Boolean]

def msetnx[V: ByteStringSerializer](keysValues: Map[String, V]): Future[Boolean]

def incrby(key: String, increment: Long): Future[Long]

def lrange[R: ByteStringDeserializer](key: String, start: Long, stop: Long): Future[Seq[R]]

def lrem[V: ByteStringSerializer](key: String, count: Long, value: V): Future[Long]

def ltrim(key: String, start: Long, stop: Long): Future[Boolean]

def linsert[V: ByteStringSerializer](key: String, beforeAfter: ListPivot, pivot: String, value: V): Future[Long]

def hincrby(key: String, field: String, increment: Long): Future[Long]

def hset[V: ByteStringSerializer](key: String, field: String, value: V): Future[Boolean]

def zcard(key: String): Future[Long]

def zscore[V: ByteStringSerializer](key: String, member: V): Future[Option[Double]]

def zrange[V: ByteStringDeserializer](key: String, start: Long, stop: Long): Future[Seq[V]]

def zrevrange[V: ByteStringDeserializer](key: String, start: Long, stop: Long): Future[Seq[V]]

def zaddMock[V: ByteStringSerializer](key: String, scoreMembers: Seq[(Double, V)]): Future[Long]

def zremMock[V: ByteStringSerializer](key: String, members: Seq[V]): Future[Long]
}

private object RedisCommandsMock {

def mock(factory: AsyncMockFactory)(implicit ec: ExecutionContext): (RedisCommands, RedisCommandsMock) = {
val mock = factory.mock[RedisCommandsMock](factory)
(new RedisCommandsAdapter(mock), mock)
}

}

private class RedisCommandsAdapter(inner: RedisCommandsMock)(implicit override val executionContext: ExecutionContext) extends RedisCommands {

override def send[T](redisCommand: RedisCommand[? <: RedisReply, T]): Future[T] =
throw new IllegalStateException(s"Uncaught call to mock: $redisCommand")

final override def get[R: ByteStringDeserializer](key: String): Future[Option[R]] =
inner.get(key)

final override def set[V: ByteStringSerializer](key: String, value: V, exSeconds: Option[Long], pxMilliseconds: Option[Long], NX: Boolean, XX: Boolean): Future[Boolean] =
inner.set(key, value, exSeconds, pxMilliseconds, NX, XX)

final override def expire(key: String, seconds: Long): Future[Boolean] =
inner.expire(key, seconds)

final override def mset[V: ByteStringSerializer](keysValues: Map[String, V]): Future[Boolean] =
inner.mset(keysValues)

final override def msetnx[V: ByteStringSerializer](keysValues: Map[String, V]): Future[Boolean] =
inner.msetnx(keysValues)

final override def incrby(key: String, increment: Long): Future[Long] =
inner.incrby(key, increment)

final override def lrange[R: ByteStringDeserializer](key: String, start: Long, stop: Long): Future[Seq[R]] =
inner.lrange(key, start, stop)

final override def lrem[V: ByteStringSerializer](key: String, count: Long, value: V): Future[Long] =
inner.lrem(key, count, value)

final override def ltrim(key: String, start: Long, stop: Long): Future[Boolean] =
inner.ltrim(key, start, stop)

final override def linsert[V: ByteStringSerializer](key: String, beforeAfter: ListPivot, pivot: String, value: V): Future[Long] =
inner.linsert(key, beforeAfter, pivot, value)

final override def hincrby(key: String, field: String, increment: Long): Future[Long] =
inner.hincrby(key, field, increment)

final override def hset[V: ByteStringSerializer](key: String, field: String, value: V): Future[Boolean] =
inner.hset(key, field, value)

final override def zcard(key: String): Future[Long] =
inner.zcard(key)

final override def zscore[V: ByteStringSerializer](key: String, member: V): Future[Option[Double]] =
inner.zscore(key, member)

final override def zrange[V: ByteStringDeserializer](key: String, start: Long, stop: Long): Future[Seq[V]] =
inner.zrange(key, start, stop)

final override def zrevrange[V: ByteStringDeserializer](key: String, start: Long, stop: Long): Future[Seq[V]] =
inner.zrevrange(key, start, stop)

final override def zadd[V: ByteStringSerializer](key: String, scoreMembers: (Double, V)*): Future[Long] =
inner.zaddMock(key, scoreMembers)

final override def zrem[V: ByteStringSerializer](key: String, members: V*): Future[Long] =
inner.zremMock(key, members)

}
Loading

0 comments on commit 6a395b2

Please sign in to comment.