Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for Scala 3 #287

Merged
merged 4 commits into from
Feb 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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
Loading