diff --git a/modules/api/src/main/scala/com/github/scoquelin/arugula/RedisCommandsClient.scala b/modules/api/src/main/scala/com/github/scoquelin/arugula/RedisCommandsClient.scala index e87b6c7..5e44c91 100644 --- a/modules/api/src/main/scala/com/github/scoquelin/arugula/RedisCommandsClient.scala +++ b/modules/api/src/main/scala/com/github/scoquelin/arugula/RedisCommandsClient.scala @@ -14,6 +14,7 @@ trait RedisCommandsClient[K, V] with RedisStringAsyncCommands[K, V] with RedisHashAsyncCommands[K, V] with RedisListAsyncCommands[K, V] + with RedisSetAsyncCommands[K, V] with RedisSortedSetAsyncCommands[K, V] with RedisServerAsyncCommands[K, V] with RedisPipelineAsyncCommands[K, V] \ No newline at end of file diff --git a/modules/api/src/main/scala/com/github/scoquelin/arugula/commands/RedisBaseAsyncCommands.scala b/modules/api/src/main/scala/com/github/scoquelin/arugula/commands/RedisBaseAsyncCommands.scala index c548f25..1e58208 100644 --- a/modules/api/src/main/scala/com/github/scoquelin/arugula/commands/RedisBaseAsyncCommands.scala +++ b/modules/api/src/main/scala/com/github/scoquelin/arugula/commands/RedisBaseAsyncCommands.scala @@ -5,3 +5,10 @@ import scala.concurrent.Future trait RedisBaseAsyncCommands[K, V] { def ping: Future[String] } + +object RedisBaseAsyncCommands { + val InitialCursor: String = "0" + + final case class ScanResults[T](cursor: String, finished: Boolean, values: T) + +} \ No newline at end of file diff --git a/modules/api/src/main/scala/com/github/scoquelin/arugula/commands/RedisHashAsyncCommands.scala b/modules/api/src/main/scala/com/github/scoquelin/arugula/commands/RedisHashAsyncCommands.scala index b06c34c..25c0ff0 100644 --- a/modules/api/src/main/scala/com/github/scoquelin/arugula/commands/RedisHashAsyncCommands.scala +++ b/modules/api/src/main/scala/com/github/scoquelin/arugula/commands/RedisHashAsyncCommands.scala @@ -3,7 +3,7 @@ package com.github.scoquelin.arugula.commands import scala.collection.immutable.ListMap import scala.concurrent.Future -import com.github.scoquelin.arugula.commands.RedisKeyAsyncCommands.ScanCursor +import com.github.scoquelin.arugula.commands.RedisBaseAsyncCommands.{ScanResults, InitialCursor} /** * Asynchronous commands for manipulating/querying Hashes (key/value pairs) @@ -133,12 +133,20 @@ trait RedisHashAsyncCommands[K, V] { * scan the fields of a hash, returning the cursor and a map of field -> value * Repeat calls with the returned cursor to get all fields until the cursor is finished * @param key The key - * @param cursor The cursor - * @param limit The maximum number of fields to return + * @param cursor The cursor to resume scanning from previous calls + * (use InitialCursor to start at the beginning). + * @param limit The maximum number of fields to return. If None, the server determines the limit. + * Note that redis may return more fields than the limit or less than the limit. This is + * a hint to the server, not a guarantee. * @param matchPattern A glob-style pattern to match fields against * @return The next cursor and a map of field -> value */ - def hScan(key: K, cursor: ScanCursor = ScanCursor.Initial, limit: Option[Long] = None, matchPattern: Option[String] = None): Future[(ScanCursor, Map[K, V])] + def hScan( + key: K, + cursor: String = InitialCursor, + limit: Option[Long] = None, + matchPattern: Option[String] = None + ): Future[ScanResults[Map[K, V]]] /** * Get the length of a hash field value diff --git a/modules/api/src/main/scala/com/github/scoquelin/arugula/commands/RedisKeyAsyncCommands.scala b/modules/api/src/main/scala/com/github/scoquelin/arugula/commands/RedisKeyAsyncCommands.scala index ee9ea64..8c3a6de 100644 --- a/modules/api/src/main/scala/com/github/scoquelin/arugula/commands/RedisKeyAsyncCommands.scala +++ b/modules/api/src/main/scala/com/github/scoquelin/arugula/commands/RedisKeyAsyncCommands.scala @@ -15,16 +15,3 @@ trait RedisKeyAsyncCommands[K, V] { def expire(key: K, expiresIn: FiniteDuration): Future[Boolean] def ttl(key: K): Future[Option[FiniteDuration]] } - -object RedisKeyAsyncCommands { - final case class ScanCursor(cursor: String, finished: Boolean) - - object ScanCursor{ - def apply(cursor: String) = new ScanCursor(cursor, finished = false) - - val Initial: ScanCursor = ScanCursor("0", finished = false) - - val Finished: ScanCursor = ScanCursor("0", finished = true) - } - -} \ No newline at end of file diff --git a/modules/api/src/main/scala/com/github/scoquelin/arugula/commands/RedisSetAsyncCommands.scala b/modules/api/src/main/scala/com/github/scoquelin/arugula/commands/RedisSetAsyncCommands.scala new file mode 100644 index 0000000..e9c65e4 --- /dev/null +++ b/modules/api/src/main/scala/com/github/scoquelin/arugula/commands/RedisSetAsyncCommands.scala @@ -0,0 +1,162 @@ +package com.github.scoquelin.arugula.commands + +import scala.concurrent.Future + +import com.github.scoquelin.arugula.commands.RedisBaseAsyncCommands.{InitialCursor, ScanResults} + +/** + * Asynchronous commands for manipulating/querying Sets + * @tparam K The key type + * @tparam V The value type + */ + +trait RedisSetAsyncCommands[K, V] { + /** + * add one or more members to a set + * @param key The key + * @param values The values to add + * @return The number of elements that were added to the set + */ + def sAdd(key: K, values: V*): Future[Long] + + /** + * get the number of members in a set + * @param key The key + * @return The number of elements in the set + */ + def sCard(key: K): Future[Long] + + /** + * get the difference between sets + * @param keys The keys + * @return The difference between the sets + */ + def sDiff(keys: K*): Future[Set[V]] + + /** + * store the difference between sets + * @param destination The destination key + * @param keys The keys + * @return The number of elements in the resulting set + */ + def sDiffStore(destination: K, keys: K*): Future[Long] + + /** + * get the intersection between sets + * @param keys The keys + * @return The intersection between the sets + */ + def sInter(keys: K*): Future[Set[V]] + + /** + * get the number of elements in the intersection between sets + * @param keys The keys + * @return The number of elements in the resulting set + */ + def sInterCard(keys: K*): Future[Long] + + /** + * get the intersection between sets and store the result + * @param destination The destination key + * @param keys The keys + * @return The number of elements in the resulting set + */ + def sInterStore(destination: K, keys: K*): Future[Long] + + /** + * determine if a member is in a set + * @param key The key + * @param value The value + * @return True if the value is in the set, false otherwise + */ + def sIsMember(key: K, value: V): Future[Boolean] + + /** + * get all the members in a set + * @param key The key + * @return A list of all the members in the set + */ + def sMembers(key: K): Future[Set[V]] + + /** + * determine if members are in a set + * @param key The key + * @param values The values + * @return A list of booleans indicating if the values are in the set + */ + def smIsMember(key: K, values: V*): Future[List[Boolean]] + + /** + * move a member from one set to another + * @param source The source key + * @param destination The destination key + * @param member The member to move + * @return True if the member was moved, false otherwise + */ + def sMove(source: K, destination: K, member: V): Future[Boolean] + + /** + * Remove and return a random member from a set. + * @param key The key + * @return The removed member, or None if the set is empty + */ + def sPop(key: K): Future[Option[V]] + + /** + * Get a random member from a set. + * @param key The key + * @return The random member, or None if the set is empty + */ + def sRandMember(key: K): Future[Option[V]] + + /** + * Get one or more random members from a set. + * @param key The key + * @param count The number of members to get + * @return The random members + */ + def sRandMember(key: K, count: Long): Future[Set[V]] + + /** + * Remove one or more members from a set + * @param key The key + * @param values The values to remove + * @return The number of members that were removed from the set + */ + def sRem(key: K, values: V*): Future[Long] + + /** + * Get the union between sets + * @param keys The keys + * @return The union between the sets + */ + def sUnion(keys: K*): Future[Set[V]] + + /** + * Store the union between sets + * @param destination The destination key + * @param keys The keys + * @return The number of elements in the resulting set + */ + def sUnionStore(destination: K, keys: K*): Future[Long] + + /** + * Incrementally iterate over a set, retrieving members in batches, using a cursor + * to resume from the last position + * @param key The key + * @param cursor The cursor + * @param limit The maximum number of elements to return + * (note: the actual number of elements returned may be less than the limit) + * (note: if the limit is None, the server will determine the number of elements to return) + * @param matchPattern The pattern to match + * (note: the pattern is a glob-style pattern) + * (note: if the pattern is None, all elements will be returned) + * @return The cursor, whether the cursor is finished, and the elements + */ + def sScan( + key: K, + cursor: String = InitialCursor, + limit: Option[Long] = None, + matchPattern: Option[String] = None + ): Future[ScanResults[Set[V]]] +} diff --git a/modules/api/src/main/scala/com/github/scoquelin/arugula/commands/RedisSortedSetAsyncCommands.scala b/modules/api/src/main/scala/com/github/scoquelin/arugula/commands/RedisSortedSetAsyncCommands.scala index c760e3d..cc58767 100644 --- a/modules/api/src/main/scala/com/github/scoquelin/arugula/commands/RedisSortedSetAsyncCommands.scala +++ b/modules/api/src/main/scala/com/github/scoquelin/arugula/commands/RedisSortedSetAsyncCommands.scala @@ -3,8 +3,9 @@ package com.github.scoquelin.arugula.commands import scala.concurrent.Future +import com.github.scoquelin.arugula.commands.RedisBaseAsyncCommands.{InitialCursor, ScanResults} + trait RedisSortedSetAsyncCommands[K, V] { - import RedisKeyAsyncCommands.ScanCursor import RedisSortedSetAsyncCommands._ def zAdd(key: K, args: Option[ZAddOptions], values: ScoreWithValue[V]*): Future[Long] def zPopMin(key: K, count: Long): Future[List[ScoreWithValue[V]]] @@ -12,14 +13,13 @@ trait RedisSortedSetAsyncCommands[K, V] { def zRangeWithScores(key: K, start: Long, stop: Long): Future[List[ScoreWithValue[V]]] def zRangeByScore[T: Numeric](key: K, range: ZRange[T], limit: Option[RangeLimit]): Future[List[V]] def zRevRangeByScore[T: Numeric](key: K, range: ZRange[T], limit: Option[RangeLimit]): Future[List[V]] - def zScan(key: K, cursor: ScanCursor = ScanCursor.Initial, limit: Option[Long] = None, matchPattern: Option[String] = None): Future[ScanCursorWithScoredValues[V]] + def zScan(key: K, cursor: String = InitialCursor, limit: Option[Long] = None, matchPattern: Option[String] = None): Future[ScanResults[List[ScoreWithValue[V]]]] def zRem(key: K, values: V*): Future[Long] def zRemRangeByRank(key: K, start: Long, stop: Long): Future[Long] def zRemRangeByScore[T: Numeric](key: K, range: ZRange[T]): Future[Long] } object RedisSortedSetAsyncCommands { - import RedisKeyAsyncCommands.ScanCursor sealed trait ZAddOptions object ZAddOptions { @@ -37,5 +37,4 @@ object RedisSortedSetAsyncCommands { final case class ScoreWithValue[V](score: Double, value: V) final case class ZRange[T](start: T, end: T) final case class RangeLimit(offset: Long, count: Long) - final case class ScanCursorWithScoredValues[V](cursor: ScanCursor, values: List[ScoreWithValue[V]]) } \ No newline at end of file diff --git a/modules/core/src/main/scala/com/github/scoquelin/arugula/LettuceRedisCommandsClient.scala b/modules/core/src/main/scala/com/github/scoquelin/arugula/LettuceRedisCommandsClient.scala index 9c7074a..675e8a1 100644 --- a/modules/core/src/main/scala/com/github/scoquelin/arugula/LettuceRedisCommandsClient.scala +++ b/modules/core/src/main/scala/com/github/scoquelin/arugula/LettuceRedisCommandsClient.scala @@ -34,6 +34,7 @@ private[arugula] class LettuceRedisCommandsClient[K, V]( with LettuceRedisServerAsyncCommands[K, V] with LettuceRedisListAsyncCommands[K, V] with LettuceRedisStringAsyncCommands[K, V] + with LettuceRedisSetAsyncCommands[K, V] with LettuceRedisSortedSetAsyncCommands[K, V] with LettuceRedisPipelineAsyncCommands[K, V] { diff --git a/modules/core/src/main/scala/com/github/scoquelin/arugula/commands/LettuceRedisHashAsyncCommands.scala b/modules/core/src/main/scala/com/github/scoquelin/arugula/commands/LettuceRedisHashAsyncCommands.scala index 8738695..f8f24c5 100644 --- a/modules/core/src/main/scala/com/github/scoquelin/arugula/commands/LettuceRedisHashAsyncCommands.scala +++ b/modules/core/src/main/scala/com/github/scoquelin/arugula/commands/LettuceRedisHashAsyncCommands.scala @@ -4,6 +4,7 @@ import scala.collection.immutable.ListMap import scala.concurrent.Future import scala.jdk.CollectionConverters._ +import com.github.scoquelin.arugula.commands.RedisBaseAsyncCommands.{InitialCursor, ScanResults} import com.github.scoquelin.arugula.internal.LettuceRedisCommandDelegation import io.lettuce.core.ScanArgs @@ -40,12 +41,7 @@ private[arugula] trait LettuceRedisHashAsyncCommands[K, V] extends RedisHashAsyn case kv if kv.hasValue => kv.getKey -> kv.getValue }.toMap) - override def hScan( - key: K, - cursor: RedisKeyAsyncCommands.ScanCursor = RedisKeyAsyncCommands.ScanCursor.Initial, - limit: Option[Long] = None, - matchPattern: Option[String] = None - ): Future[(RedisKeyAsyncCommands.ScanCursor, Map[K, V])] = { + override def hScan(key: K, cursor: String = InitialCursor, limit: Option[Long] = None, matchPattern: Option[String] = None): Future[ScanResults[Map[K, V]]] = { val scanArgs = (limit, matchPattern) match { case (Some(limitValue), Some(matchPatternValue)) => Some(ScanArgs.Builder.limit(limitValue).`match`(matchPatternValue)) @@ -56,8 +52,7 @@ private[arugula] trait LettuceRedisHashAsyncCommands[K, V] extends RedisHashAsyn case _ => None } - val lettuceCursor = io.lettuce.core.ScanCursor.of(cursor.cursor) - lettuceCursor.setFinished(cursor.finished) + val lettuceCursor = io.lettuce.core.ScanCursor.of(cursor) val response = scanArgs match { case Some(scanArgs) => delegateRedisClusterCommandAndLift(_.hscan(key, lettuceCursor, scanArgs)) @@ -65,9 +60,10 @@ private[arugula] trait LettuceRedisHashAsyncCommands[K, V] extends RedisHashAsyn delegateRedisClusterCommandAndLift(_.hscan(key, lettuceCursor)) } response.map{ result => - ( - RedisKeyAsyncCommands.ScanCursor(result.getCursor, finished = result.isFinished), - result.getMap.asScala.toMap + ScanResults( + cursor = result.getCursor, + finished = result.isFinished, + values = result.getMap.asScala.toMap ) } } diff --git a/modules/core/src/main/scala/com/github/scoquelin/arugula/commands/LettuceRedisSetAsyncCommands.scala b/modules/core/src/main/scala/com/github/scoquelin/arugula/commands/LettuceRedisSetAsyncCommands.scala new file mode 100644 index 0000000..7f04449 --- /dev/null +++ b/modules/core/src/main/scala/com/github/scoquelin/arugula/commands/LettuceRedisSetAsyncCommands.scala @@ -0,0 +1,96 @@ +package com.github.scoquelin.arugula.commands + +import scala.concurrent.Future +import scala.jdk.CollectionConverters.CollectionHasAsScala + +import com.github.scoquelin.arugula.commands.RedisBaseAsyncCommands.{InitialCursor, ScanResults} +import com.github.scoquelin.arugula.internal.LettuceRedisCommandDelegation + + + +private[arugula] trait LettuceRedisSetAsyncCommands[K, V] extends RedisSetAsyncCommands[K, V] with LettuceRedisCommandDelegation[K, V] { + + override def sAdd(key: K, values: V*): Future[Long] = + delegateRedisClusterCommandAndLift(_.sadd(key, values: _*)).map(Long2long) + + override def sCard(key: K): Future[Long] = + delegateRedisClusterCommandAndLift(_.scard(key)).map(Long2long) + + override def sDiff(keys: K*): Future[Set[V]] = + delegateRedisClusterCommandAndLift(_.sdiff(keys: _*)).map(_.asScala.toSet) + + override def sDiffStore(destination: K, keys: K*): Future[Long] = + delegateRedisClusterCommandAndLift(_.sdiffstore(destination, keys: _*)).map(Long2long) + + override def sInter(keys: K*): Future[Set[V]] = + delegateRedisClusterCommandAndLift(_.sinter(keys: _*)).map(_.asScala.toSet) + + override def sInterCard(keys: K*): Future[Long] = + delegateRedisClusterCommandAndLift(_.sintercard(keys: _*)).map(Long2long) + + override def sInterStore(destination: K, keys: K*): Future[Long] = + delegateRedisClusterCommandAndLift(_.sinterstore(destination, keys: _*)).map(Long2long) + + override def sIsMember(key: K, value: V): Future[Boolean] = + delegateRedisClusterCommandAndLift(_.sismember(key, value)).map(Boolean2boolean) + + override def sMembers(key: K): Future[Set[V]] = + delegateRedisClusterCommandAndLift(_.smembers(key)).map(_.asScala.toSet) + + override def smIsMember(key: K, values: V*): Future[List[Boolean]] = + delegateRedisClusterCommandAndLift(_.smismember(key, values:_*)).map(_.asScala.toList.map(Boolean2boolean)) + + override def sMove(source: K, destination: K, member: V): Future[Boolean] = + delegateRedisClusterCommandAndLift(_.smove(source, destination, member)).map(Boolean2boolean) + + override def sPop(key: K): Future[Option[V]] = + delegateRedisClusterCommandAndLift(_.spop(key)).map(Option.apply) + + override def sRandMember(key: K): Future[Option[V]] = + delegateRedisClusterCommandAndLift(_.srandmember(key)).map(Option.apply) + + override def sRandMember(key: K, count: Long): Future[Set[V]] = + delegateRedisClusterCommandAndLift(_.srandmember(key, count)).map(_.asScala.toSet) + + override def sRem(key: K, values: V*): Future[Long] = + delegateRedisClusterCommandAndLift(_.srem(key, values: _*)).map(Long2long) + + override def sUnion(keys: K*): Future[Set[V]] = + delegateRedisClusterCommandAndLift(_.sunion(keys: _*)).map(_.asScala.toSet) + + override def sUnionStore(destination: K, keys: K*): Future[Long] = + delegateRedisClusterCommandAndLift(_.sunionstore(destination, keys: _*)).map(Long2long) + + override def sScan( + key: K, + cursor: String = InitialCursor, + limit: Option[Long] = None, + matchPattern: Option[String] = None + ): Future[ScanResults[Set[V]]] = { + val scanArgs = (limit, matchPattern) match { + case (Some(limitValue), Some(matchPatternValue)) => + Some(io.lettuce.core.ScanArgs.Builder.limit(limitValue).`match`(matchPatternValue)) + case (Some(limitValue), None) => + Some(io.lettuce.core.ScanArgs.Builder.limit(limitValue)) + case (None, Some(matchPatternValue)) => + Some(io.lettuce.core.ScanArgs.Builder.matches(matchPatternValue)) + case _ => + None + } + val lettuceCursor = io.lettuce.core.ScanCursor.of(cursor) + val response = scanArgs match { + case Some(scanArgs) => + delegateRedisClusterCommandAndLift(_.sscan(key, lettuceCursor, scanArgs)) + case None => + delegateRedisClusterCommandAndLift(_.sscan(key, lettuceCursor)) + } + response.map{ result => + ScanResults( + cursor = result.getCursor, + finished = result.isFinished, + result.getValues.asScala.toSet + ) + } + } + +} \ No newline at end of file diff --git a/modules/core/src/main/scala/com/github/scoquelin/arugula/commands/LettuceRedisSortedSetAsyncCommands.scala b/modules/core/src/main/scala/com/github/scoquelin/arugula/commands/LettuceRedisSortedSetAsyncCommands.scala index d3215ee..e47fbe9 100644 --- a/modules/core/src/main/scala/com/github/scoquelin/arugula/commands/LettuceRedisSortedSetAsyncCommands.scala +++ b/modules/core/src/main/scala/com/github/scoquelin/arugula/commands/LettuceRedisSortedSetAsyncCommands.scala @@ -2,12 +2,12 @@ package com.github.scoquelin.arugula.commands import scala.concurrent.Future -import com.github.scoquelin.arugula.commands.RedisKeyAsyncCommands.ScanCursor import com.github.scoquelin.arugula.commands.RedisSortedSetAsyncCommands.ZAddOptions.{CH, GT, LT, NX, XX} -import com.github.scoquelin.arugula.commands.RedisSortedSetAsyncCommands.{RangeLimit, ScanCursorWithScoredValues, ScoreWithValue, ZAddOptions, ZRange} +import com.github.scoquelin.arugula.commands.RedisSortedSetAsyncCommands.{RangeLimit, ScoreWithValue, ZAddOptions, ZRange} import io.lettuce.core.{Limit, Range, ScanArgs, ScoredValue, ZAddArgs} import scala.jdk.CollectionConverters._ +import com.github.scoquelin.arugula.commands.RedisBaseAsyncCommands.{InitialCursor, ScanResults} import com.github.scoquelin.arugula.internal.LettuceRedisCommandDelegation private[arugula] trait LettuceRedisSortedSetAsyncCommands[K, V] extends RedisSortedSetAsyncCommands[K, V] with LettuceRedisCommandDelegation[K, V] { @@ -56,7 +56,7 @@ private[arugula] trait LettuceRedisSortedSetAsyncCommands[K, V] extends RedisSor delegateRedisClusterCommandAndLift(_.zrangeWithScores(key, start, stop)) .map(_.asScala.toList.map(scoredValue => ScoreWithValue(scoredValue.getScore, scoredValue.getValue))) - override def zScan(key: K, cursor: ScanCursor = ScanCursor.Initial, limit: Option[Long] = None, matchPattern: Option[String] = None): Future[ScanCursorWithScoredValues[V]] = { + override def zScan(key: K, cursor: String = InitialCursor, limit: Option[Long] = None, matchPattern: Option[String] = None): Future[ScanResults[List[ScoreWithValue[V]]]] = { val scanArgs = (limit, matchPattern) match { case (Some(limitValue), Some(matchPatternValue)) => Some(ScanArgs.Builder.limit(limitValue).`match`(matchPatternValue)) @@ -70,12 +70,20 @@ private[arugula] trait LettuceRedisSortedSetAsyncCommands[K, V] extends RedisSor scanArgs match { case Some(args) => - delegateRedisClusterCommandAndLift(_.zscan(key, io.lettuce.core.ScanCursor.of(cursor.cursor), args)).map { scanResult => - ScanCursorWithScoredValues(ScanCursor(scanResult.getCursor, scanResult.isFinished), scanResult.getValues.asScala.toList.map(scoredValue => ScoreWithValue(scoredValue.getScore, scoredValue.getValue))) + delegateRedisClusterCommandAndLift(_.zscan(key, io.lettuce.core.ScanCursor.of(cursor), args)).map { scanResult => + ScanResults( + cursor = scanResult.getCursor, + finished = scanResult.isFinished, + values = scanResult.getValues.asScala.toList.map(scoredValue => ScoreWithValue(scoredValue.getScore, scoredValue.getValue)) + ) } case None => - delegateRedisClusterCommandAndLift(_.zscan(key, io.lettuce.core.ScanCursor.of(cursor.cursor))).map { scanResult => - ScanCursorWithScoredValues(ScanCursor(scanResult.getCursor, scanResult.isFinished), scanResult.getValues.asScala.toList.map(scoredValue => ScoreWithValue(scoredValue.getScore, scoredValue.getValue))) + delegateRedisClusterCommandAndLift(_.zscan(key, io.lettuce.core.ScanCursor.of(cursor))).map { scanResult => + ScanResults( + cursor = scanResult.getCursor, + finished = scanResult.isFinished, + values = scanResult.getValues.asScala.toList.map(scoredValue => ScoreWithValue(scoredValue.getScore, scoredValue.getValue)) + ) } } } diff --git a/modules/tests/it/src/test/scala/com/github/scoquelin/arugula/RedisCommandsIntegrationSpec.scala b/modules/tests/it/src/test/scala/com/github/scoquelin/arugula/RedisCommandsIntegrationSpec.scala index da6cf1b..b8dc36e 100644 --- a/modules/tests/it/src/test/scala/com/github/scoquelin/arugula/RedisCommandsIntegrationSpec.scala +++ b/modules/tests/it/src/test/scala/com/github/scoquelin/arugula/RedisCommandsIntegrationSpec.scala @@ -1,12 +1,14 @@ package com.github.scoquelin.arugula import scala.collection.immutable.ListMap +import scala.concurrent.Future import com.github.scoquelin.arugula.codec.RedisCodec import com.github.scoquelin.arugula.commands.RedisSortedSetAsyncCommands.{RangeLimit, ScoreWithValue, ZAddOptions, ZRange} import org.scalatest.matchers.should.Matchers import scala.concurrent.duration._ +import com.github.scoquelin.arugula.commands.RedisBaseAsyncCommands.InitialCursor import com.github.scoquelin.arugula.commands.RedisListAsyncCommands import com.github.scoquelin.arugula.commands.RedisStringAsyncCommands.{BitFieldCommand, BitFieldDataType} @@ -429,16 +431,16 @@ class RedisCommandsIntegrationSpec extends BaseRedisCommandsIntegrationSpec with revRangeByScore <- client.zRevRangeByScore(key, ZRange(0, 2), Some(RangeLimit(0, 2))) _ <- revRangeByScore.shouldBe(List("two", "one")) zScan <- client.zScan(key) - _ <- zScan.cursor.finished shouldBe true + _ <- zScan.finished shouldBe true _ <- zScan.values shouldBe List(ScoreWithValue(1, "one"), ScoreWithValue(2, "two"), ScoreWithValue(3, "three"), ScoreWithValue(4, "four"), ScoreWithValue(5, "five")) zScanWithMatch <- client.zScan(key, matchPattern = Some("t*")) - _ <- zScanWithMatch.cursor.finished shouldBe true + _ <- zScanWithMatch.finished shouldBe true _ <- zScanWithMatch.values shouldBe List(ScoreWithValue(2, "two"), ScoreWithValue(3, "three")) zScanWithLimit <- client.zScan(key, limit = Some(10)) - _ <- zScanWithLimit.cursor.finished shouldBe true + _ <- zScanWithLimit.finished shouldBe true _ <- zScanWithLimit.values shouldBe List(ScoreWithValue(1, "one"), ScoreWithValue(2, "two"), ScoreWithValue(3, "three"), ScoreWithValue(4, "four"), ScoreWithValue(5, "five")) zScanWithMatchAndLimit <- client.zScan(key, matchPattern = Some("t*"), limit = Some(10)) - _ <- zScanWithMatchAndLimit.cursor.finished shouldBe true + _ <- zScanWithMatchAndLimit.finished shouldBe true _ <- zScanWithMatchAndLimit.values shouldBe List(ScoreWithValue(2, "two"), ScoreWithValue(3, "three")) zRemRangeByRank <- client.zRemRangeByRank(key, 0, 0) _ <- zRemRangeByRank shouldBe 1L @@ -517,18 +519,18 @@ class RedisCommandsIntegrationSpec extends BaseRedisCommandsIntegrationSpec with hStrLen <- client.hStrLen(key, field) _ <- hStrLen shouldBe 5L hScanResults <- client.hScan(key) - _ <- hScanResults._1.finished shouldBe true - _ <- hScanResults._2 shouldBe Map(field -> value) + _ <- hScanResults.finished shouldBe true + _ <- hScanResults.values shouldBe Map(field -> value) _ <- client.del(key) _ <- client.hMSet(key, Map("field1" -> "value1", "field2" -> "value2", "extraField3" -> "value3")) fieldValues <- client.hGetAll(key) _ <- fieldValues shouldBe Map("field1" -> "value1", "field2" -> "value2", "extraField3" -> "value3") scanResultsWithFilter <- client.hScan(key, matchPattern = Some("field*")) - _ <- scanResultsWithFilter._1.finished shouldBe true - _ <- scanResultsWithFilter._2 shouldBe Map("field1" -> "value1", "field2" -> "value2") + _ <- scanResultsWithFilter.finished shouldBe true + _ <- scanResultsWithFilter.values shouldBe Map("field1" -> "value1", "field2" -> "value2") scanResultsWithLimit <- client.hScan(key, limit = Some(10)) - _ <- scanResultsWithLimit._1.finished shouldBe true - _ <- scanResultsWithLimit._2 shouldBe Map("field1" -> "value1", "field2" -> "value2", "extraField3" -> "value3") + _ <- scanResultsWithLimit.finished shouldBe true + _ <- scanResultsWithLimit.values shouldBe Map("field1" -> "value1", "field2" -> "value2", "extraField3" -> "value3") randomFieldsWithValues <- client.hRandFieldWithValues(key, 3) _ <- randomFieldsWithValues shouldBe Map("field1" -> "value1", "field2" -> "value2", "extraField3" -> "value3") } yield succeed @@ -559,6 +561,124 @@ class RedisCommandsIntegrationSpec extends BaseRedisCommandsIntegrationSpec with } } + "leveraging RedisSetAsyncCommands" should { + "create, retrieve, pop, and remove values in a set" in { + withRedisSingleNodeAndCluster(RedisCodec.Utf8WithValueAsStringCodec) { client => + val key = randomKey("set") + val values = List("one", "two", "three") + for { + addResults <- client.sAdd(key, values: _*) + _ <- addResults shouldBe values.size + members <- client.sMembers(key) + _ <- members shouldBe values.toSet + isMember <- client.sIsMember(key, "two") + _ <- isMember shouldBe true + isMember <- client.sIsMember(key, "four") + _ <- isMember shouldBe false + multiIsMember <- client.smIsMember(key, "one", "two", "three", "four") + _ <- multiIsMember shouldBe List(true, true, true, false) + cardResult <- client.sCard(key) + _ <- cardResult shouldBe values.size + popResult <- client.sPop(key) + _ <- popResult shouldBe defined + removeResult <- client.sRem(key, values: _*) + _ <- removeResult shouldBe 2 + } yield succeed + } + } + + "support random member operations" in { + withRedisSingleNodeAndCluster(RedisCodec.Utf8WithValueAsStringCodec) { client => + val key = randomKey("set") + val values = List("one", "two", "three") + for { + addResults <- client.sAdd(key, values: _*) + _ <- addResults shouldBe values.size + randResult <- client.sRandMember(key) + _ <- randResult shouldBe defined + randResults <- client.sRandMember(key, 2) + _ <- randResults.size shouldBe 2 + } yield succeed + } + } + + "support multi-key union, diffing, and moving operations" in { + withRedisSingleNodeAndCluster(RedisCodec.Utf8WithValueAsStringCodec) { client => + val suffix = "{user1}" + val key1 = randomKey("set-1") + suffix + val key2 = randomKey("set-2") + suffix + val key3 = randomKey("set-3") + suffix + val values = List("one", "two", "three") + for { + addResults <- client.sAdd(key1, values: _*) + _ <- addResults shouldBe values.size + addResults <- client.sAdd(key2, values: _*) + _ <- addResults shouldBe values.size + addResults <- client.sAdd(key3, values: _*) + _ <- addResults shouldBe values.size + sDiffResults <- client.sDiff(key1, key2, key3) + _ <- sDiffResults shouldBe Set() + sInterResults <- client.sInter(key1, key2, key3) + _ <- sInterResults shouldBe values.toSet + sUnionResults <- client.sUnion(key1, key2, key3) + _ <- sUnionResults shouldBe values.toSet + sDiffStoreResults <- client.sDiffStore(key1, key2, key3, key3) + _ <- sDiffStoreResults shouldBe 0 + sInterStoreResults <- client.sInterStore(key1, key2, key3, key3) + _ <- sInterStoreResults shouldBe values.size + sUnionStoreResults <- client.sUnionStore(key1, key2, key3, key3) + _ <- sUnionStoreResults shouldBe values.size + interCardResult <- client.sInterCard(key1, key2, key3) + _ <- interCardResult shouldBe values.size + moveResult <- client.sMove(key1, key2, "two") + _ <- moveResult shouldBe true + diffResult <- client.sDiff(key2, key1) + _ <- diffResult shouldBe Set("two") + unionResult <- client.sUnion(key1, key2) + _ <- unionResult shouldBe values.toSet + } yield succeed + } + } + + + "support scanning a set" in { + withRedisSingleNodeAndCluster(RedisCodec.Utf8WithValueAsStringCodec) { client => + val key = randomKey("large-set") + val members = (1 to 1000).map(_.toString).toList + + def scanAll(cursor: String = InitialCursor, accumulated: Set[String] = Set.empty): Future[Set[String]] = { + client.sScan(key, cursor).flatMap { scanResult => + val newAccumulated = accumulated ++ scanResult.values + if (scanResult.finished) Future.successful(newAccumulated) + else scanAll(scanResult.cursor, newAccumulated) + } + } + + for { + addResults <- client.sAdd(key, members: _*) + _ <- addResults shouldBe members.size + scanResult <- client.sScan(key) + _ <- scanResult.finished shouldBe false + _ <- scanResult.values.size shouldBe >=(1) + scanResultNext <- client.sScan(key, cursor = scanResult.cursor) + _ <- scanResultNext.finished shouldBe false + _ <- scanResultNext.values.size shouldBe >=(2) + scanResultWithMatch <- client.sScan(key, matchPattern = Some("1*")) + _ <- scanResultWithMatch.finished shouldBe false + _ <- members.filter(_.startsWith("1")).toSet should contain allElementsOf scanResultWithMatch.values + scanResultWithLimit <- client.sScan(key, limit = Some(3)) + _ <- scanResultWithLimit.finished shouldBe false + _ <- members should contain allElementsOf scanResultWithLimit.values + + // start with initial cursor and fetch until finished + scanResult <- scanAll() + _ <- scanResult.size shouldEqual members.size + _ <- scanResult shouldBe members.toSet + } yield succeed + } + } + } + "leveraging RedisPipelineAsyncCommands" should { "allow to send a batch of commands using pipeline" in { diff --git a/modules/tests/test/src/test/scala/com/github/scoquelin/arugula/LettuceRedisHashAsyncCommandsSpec.scala b/modules/tests/test/src/test/scala/com/github/scoquelin/arugula/LettuceRedisHashAsyncCommandsSpec.scala index a98db1b..e8a3651 100644 --- a/modules/tests/test/src/test/scala/com/github/scoquelin/arugula/LettuceRedisHashAsyncCommandsSpec.scala +++ b/modules/tests/test/src/test/scala/com/github/scoquelin/arugula/LettuceRedisHashAsyncCommandsSpec.scala @@ -2,7 +2,6 @@ package com.github.scoquelin.arugula import scala.collection.immutable.ListMap -import com.github.scoquelin.arugula.commands.RedisKeyAsyncCommands.ScanCursor import io.lettuce.core.{KeyValue, MapScanCursor, RedisFuture} import org.mockito.ArgumentMatchers.{any, anyLong, anyString, eq => meq} import org.mockito.Mockito.{verify, when} @@ -10,6 +9,8 @@ import org.scalatest.matchers.must.Matchers import org.scalatest.{FutureOutcome, wordspec} import scala.jdk.CollectionConverters._ +import com.github.scoquelin.arugula.commands.RedisBaseAsyncCommands.ScanResults + class LettuceRedisHashAsyncCommandsSpec extends wordspec.FixtureAsyncWordSpec with Matchers { @@ -253,7 +254,7 @@ class LettuceRedisHashAsyncCommandsSpec extends wordspec.FixtureAsyncWordSpec wi "delegate HSCAN command to Lettuce and lift result into a Future" in { testContext => import testContext._ - val expectedValue = (ScanCursor("0", finished = false), Map("field1" -> "value1", "field2" -> "value2")) + val expectedValue = ScanResults(cursor = "0", finished = false, values = Map("field1" -> "value1", "field2" -> "value2")) val mapScanCursor = new MapScanCursor[String, String]() mapScanCursor.getMap.put("field1", "value1") mapScanCursor.getMap.put("field2", "value2") @@ -274,7 +275,7 @@ class LettuceRedisHashAsyncCommandsSpec extends wordspec.FixtureAsyncWordSpec wi "delegate HSCAN command with cursor and match options to Lettuce and lift result into a Future" in { testContext => import testContext._ - val expectedValue = (ScanCursor("1", finished = true), Map("field3" -> "value3", "field4" -> "value4")) + val expectedValue = ScanResults("1", finished = true, values = Map("field3" -> "value3", "field4" -> "value4")) val mapScanCursor = new MapScanCursor[String, String]() mapScanCursor.getMap.put("field3", "value3") mapScanCursor.getMap.put("field4", "value4") @@ -285,7 +286,7 @@ class LettuceRedisHashAsyncCommandsSpec extends wordspec.FixtureAsyncWordSpec wi ) when(lettuceAsyncCommands.hscan(anyString, any[io.lettuce.core.ScanCursor], any)).thenReturn(mockRedisFuture) - testClass.hScan("key", ScanCursor("0", finished = false), matchPattern = Some("field*")).map { result => + testClass.hScan("key", matchPattern = Some("field*")).map { result => result mustBe expectedValue verify(lettuceAsyncCommands).hscan(meq("key"), any[io.lettuce.core.ScanCursor], any[io.lettuce.core.ScanArgs]) succeed diff --git a/modules/tests/test/src/test/scala/com/github/scoquelin/arugula/LettuceRedisSetAsyncCommandsSpec.scala b/modules/tests/test/src/test/scala/com/github/scoquelin/arugula/LettuceRedisSetAsyncCommandsSpec.scala new file mode 100644 index 0000000..dfd3d6d --- /dev/null +++ b/modules/tests/test/src/test/scala/com/github/scoquelin/arugula/LettuceRedisSetAsyncCommandsSpec.scala @@ -0,0 +1,281 @@ +package com.github.scoquelin.arugula + +import scala.jdk.CollectionConverters._ + +import com.github.scoquelin.arugula.commands.RedisBaseAsyncCommands.{InitialCursor, ScanResults} +import io.lettuce.core.{RedisFuture, ValueScanCursor} +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers.{any, eq => meq} +import org.mockito.Mockito.{verify, when} +import org.scalatest.matchers.must.Matchers +import org.scalatest.{FutureOutcome, wordspec} + +class LettuceRedisSetAsyncCommandsSpec extends wordspec.FixtureAsyncWordSpec with Matchers { + + override type FixtureParam = LettuceRedisCommandsClientFixture.TestContext + + override def withFixture(test: OneArgAsyncTest): FutureOutcome = + withFixture(test.toNoArgAsyncTest(new LettuceRedisCommandsClientFixture.TestContext)) + + "LettuceRedisSetAsyncCommands" should { + "delegate SADD command to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue: Long = 1L + val mockRedisFuture: RedisFuture[java.lang.Long] = mockRedisFutureToReturn(expectedValue) + when(lettuceAsyncCommands.sadd(any, any)).thenReturn(mockRedisFuture) + + testClass.sAdd("key", "value").map { result => + result mustBe expectedValue + verify(lettuceAsyncCommands).sadd("key", "value") + succeed + } + } + + "delegate SCARD command to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue: Long = 1L + val mockRedisFuture: RedisFuture[java.lang.Long] = mockRedisFutureToReturn(expectedValue) + when(lettuceAsyncCommands.scard(any)).thenReturn(mockRedisFuture) + + testClass.sCard("key").map { result => + result mustBe expectedValue + verify(lettuceAsyncCommands).scard("key") + succeed + } + } + + "delegate SDIFF command to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue = Set("value1", "value2") + val mockRedisFuture: RedisFuture[java.util.Set[String]] = mockRedisFutureToReturn(expectedValue.asJava) + when(lettuceAsyncCommands.sdiff(any[String], any[String])).thenReturn(mockRedisFuture) + + testClass.sDiff("key1", "key2").map { result => + result mustBe expectedValue + verify(lettuceAsyncCommands).sdiff("key1", "key2") + succeed + } + } + + "delegate SDIFFSTORE command to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue: Long = 1L + val mockRedisFuture: RedisFuture[java.lang.Long] = mockRedisFutureToReturn(expectedValue) + when(lettuceAsyncCommands.sdiffstore(any[String], any[String], any[String])).thenReturn(mockRedisFuture) + + testClass.sDiffStore("destination", "key1", "key2").map { result => + result mustBe expectedValue + verify(lettuceAsyncCommands).sdiffstore("destination", "key1", "key2") + succeed + } + } + + "delegate SINTER command to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue = Set("value1", "value2") + val mockRedisFuture: RedisFuture[java.util.Set[String]] = mockRedisFutureToReturn(expectedValue.asJava) + when(lettuceAsyncCommands.sinter(any[String], any[String])).thenReturn(mockRedisFuture) + + testClass.sInter("key1", "key2").map { result => + result mustBe expectedValue + verify(lettuceAsyncCommands).sinter("key1", "key2") + succeed + } + } + + "delegate SINTERCARD command to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue: Long = 1L + val mockRedisFuture: RedisFuture[java.lang.Long] = mockRedisFutureToReturn(expectedValue) + when(lettuceAsyncCommands.sintercard(any[String], any[String])).thenReturn(mockRedisFuture) + + testClass.sInterCard("key1", "key2").map { result => + result mustBe expectedValue + verify(lettuceAsyncCommands).sintercard("key1", "key2") + succeed + } + } + + "delegate SINTERSTORE command to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue: Long = 1L + val mockRedisFuture: RedisFuture[java.lang.Long] = mockRedisFutureToReturn(expectedValue) + when(lettuceAsyncCommands.sinterstore(any[String], any[String], any[String])).thenReturn(mockRedisFuture) + + testClass.sInterStore("destination", "key1", "key2").map { result => + result mustBe expectedValue + verify(lettuceAsyncCommands).sinterstore("destination", "key1", "key2") + succeed + } + } + + "delegate SISMEMBER command to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue: Boolean = true + val mockRedisFuture: RedisFuture[java.lang.Boolean] = mockRedisFutureToReturn(expectedValue) + when(lettuceAsyncCommands.sismember(any[String], any[String])).thenReturn(mockRedisFuture) + + testClass.sIsMember("key", "value").map { result => + result mustBe expectedValue + verify(lettuceAsyncCommands).sismember("key", "value") + succeed + } + } + + "delegate SMEMBERS command to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue = Set("value1", "value2") + val mockRedisFuture: RedisFuture[java.util.Set[String]] = mockRedisFutureToReturn(expectedValue.asJava) + when(lettuceAsyncCommands.smembers(any[String])).thenReturn(mockRedisFuture) + + testClass.sMembers("key").map { result => + result mustBe expectedValue + verify(lettuceAsyncCommands).smembers("key") + succeed + } + } + + "delegate SMISMEMBER command to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue = List(true, false) + val mockRedisFuture: RedisFuture[java.util.List[java.lang.Boolean]] = mockRedisFutureToReturn(expectedValue.map(Boolean.box).asJava) + when(lettuceAsyncCommands.smismember(any[String], any[String], any[String])).thenReturn(mockRedisFuture) + + testClass.smIsMember("key1", "value1", "value2").map { result => + result mustBe expectedValue + verify(lettuceAsyncCommands).smismember("key1", "value1", "value2") + succeed + } + } + + "delegate SMOVE command to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue: Boolean = true + val mockRedisFuture: RedisFuture[java.lang.Boolean] = mockRedisFutureToReturn(expectedValue) + when(lettuceAsyncCommands.smove(any[String], any[String], any[String])).thenReturn(mockRedisFuture) + + testClass.sMove("source", "destination", "value").map { result => + result mustBe expectedValue + verify(lettuceAsyncCommands).smove("source", "destination", "value") + succeed + } + } + + "delegate SPOP command to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue = "value" + val mockRedisFuture: RedisFuture[String] = mockRedisFutureToReturn(expectedValue) + when(lettuceAsyncCommands.spop(any[String])).thenReturn(mockRedisFuture) + + testClass.sPop("key").map { result => + result mustBe Some(expectedValue) + verify(lettuceAsyncCommands).spop("key") + succeed + } + } + + "delegate SRANDMEMBER command to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue = "value" + val mockRedisFuture: RedisFuture[String] = mockRedisFutureToReturn(expectedValue) + when(lettuceAsyncCommands.srandmember(any[String])).thenReturn(mockRedisFuture) + + testClass.sRandMember("key").map { result => + result mustBe Some(expectedValue) + verify(lettuceAsyncCommands).srandmember("key") + succeed + } + } + + "delegate SRANDMEMBER command with count to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue = Set("value1", "value2") + val mockRedisFuture: RedisFuture[java.util.List[String]] = mockRedisFutureToReturn(expectedValue.toList.asJava) + when(lettuceAsyncCommands.srandmember(any[String], any[Int])).thenReturn(mockRedisFuture) + + testClass.sRandMember("key", 2).map { result => + result mustBe expectedValue + verify(lettuceAsyncCommands).srandmember("key", 2) + succeed + } + } + + "delegate SREM command to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue: Long = 1L + val mockRedisFuture: RedisFuture[java.lang.Long] = mockRedisFutureToReturn(expectedValue) + when(lettuceAsyncCommands.srem(any[String], any[String])).thenReturn(mockRedisFuture) + + testClass.sRem("key", "value").map { result => + result mustBe expectedValue + verify(lettuceAsyncCommands).srem("key", "value") + succeed + } + } + + "delegate SUNION command to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue = Set("value1", "value2") + val mockRedisFuture: RedisFuture[java.util.Set[String]] = mockRedisFutureToReturn(expectedValue.asJava) + when(lettuceAsyncCommands.sunion(any[String], any[String])).thenReturn(mockRedisFuture) + + testClass.sUnion("key1", "key2").map { result => + result mustBe expectedValue + verify(lettuceAsyncCommands).sunion("key1", "key2") + succeed + } + } + + "delegate SUNIONSTORE command to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue: Long = 1L + val mockRedisFuture: RedisFuture[java.lang.Long] = mockRedisFutureToReturn(expectedValue) + when(lettuceAsyncCommands.sunionstore(any[String], any[String], any[String])).thenReturn(mockRedisFuture) + + testClass.sUnionStore("destination", "key1", "key2").map { result => + result mustBe expectedValue + verify(lettuceAsyncCommands).sunionstore("destination", "key1", "key2") + succeed + } + } + + "delegate SSCAN command to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue = ScanResults(cursor = "1", finished = true, values = Set("value1", "value2")) + val valueScanCursor = new ValueScanCursor[String]() + valueScanCursor.setCursor(expectedValue.cursor) + valueScanCursor.setFinished(expectedValue.finished) + valueScanCursor.getValues.addAll(expectedValue.values.asJava) + val mockRedisFuture: RedisFuture[ValueScanCursor[String]] = mockRedisFutureToReturn(valueScanCursor) + when(lettuceAsyncCommands.sscan(any[String], any[io.lettuce.core.ScanCursor])).thenReturn(mockRedisFuture) + + testClass.sScan("key").map { result => + result mustBe expectedValue + val cursorCaptor = ArgumentCaptor.forClass(classOf[io.lettuce.core.ScanCursor]) + verify(lettuceAsyncCommands).sscan(meq("key"), cursorCaptor.capture()) + val cursor = cursorCaptor.getValue.asInstanceOf[io.lettuce.core.ScanCursor] + cursor.getCursor mustBe InitialCursor + succeed + } + } + } + +} diff --git a/modules/tests/test/src/test/scala/com/github/scoquelin/arugula/LettuceRedisSortedSetAsyncCommandsSpec.scala b/modules/tests/test/src/test/scala/com/github/scoquelin/arugula/LettuceRedisSortedSetAsyncCommandsSpec.scala index 3ee5646..40b8e1a 100644 --- a/modules/tests/test/src/test/scala/com/github/scoquelin/arugula/LettuceRedisSortedSetAsyncCommandsSpec.scala +++ b/modules/tests/test/src/test/scala/com/github/scoquelin/arugula/LettuceRedisSortedSetAsyncCommandsSpec.scala @@ -127,8 +127,8 @@ class LettuceRedisSortedSetAsyncCommandsSpec extends wordspec.FixtureAsyncWordSp when(lettuceAsyncCommands.zscan(any[String], any[io.lettuce.core.ScanCursor])).thenReturn(mockRedisFuture) testClass.zScan("key").map { result => - result.cursor.cursor mustBe "1" - result.cursor.finished mustBe false + result.cursor mustBe "1" + result.finished mustBe false result.values mustBe List(ScoreWithValue(1, "one")) val cursorCaptor = ArgumentCaptor.forClass(classOf[io.lettuce.core.ScanCursor]) verify(lettuceAsyncCommands).zscan(meq("key"), cursorCaptor.capture().asInstanceOf[io.lettuce.core.ScanCursor]) @@ -151,8 +151,8 @@ class LettuceRedisSortedSetAsyncCommandsSpec extends wordspec.FixtureAsyncWordSp when(lettuceAsyncCommands.zscan(any[String], any[io.lettuce.core.ScanCursor], any[io.lettuce.core.ScanArgs])).thenReturn(mockRedisFuture) testClass.zScan("key", matchPattern = Some("o*"), limit = Some(10)).map { result => - result.cursor.cursor mustBe "1" - result.cursor.finished mustBe false + result.cursor mustBe "1" + result.finished mustBe false result.values mustBe List(ScoreWithValue(1, "one")) val cursorCaptor = ArgumentCaptor.forClass(classOf[io.lettuce.core.ScanCursor]) val scanArgsCaptor = ArgumentCaptor.forClass(classOf[io.lettuce.core.ScanArgs])