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 cc58767..f364063 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 @@ -5,34 +5,330 @@ import scala.concurrent.Future import com.github.scoquelin.arugula.commands.RedisBaseAsyncCommands.{InitialCursor, ScanResults} +/** + * Asynchronous commands for manipulating/querying Sorted Sets + * @tparam K The key type + * @tparam V The value type + */ trait RedisSortedSetAsyncCommands[K, V] { import RedisSortedSetAsyncCommands._ - def zAdd(key: K, args: Option[ZAddOptions], values: ScoreWithValue[V]*): Future[Long] + + /** + * Add one or more members to a sorted set, or update its score if it already exists + * @param key The key + * @param values The values to add + * @return The number of elements added to the sorted set + */ + def zAdd(key: K, values: ScoreWithValue[V]*): Future[Long] + + /** + * Add one or more members to a sorted set, or update its score if it already exists + * @param key The key + * @param args Optional arguments + * @param values The values to add + * @return The number of elements added to the sorted set + */ + def zAdd(key: K, args: ZAddOptions, values: ScoreWithValue[V]*): Future[Long] + + /** + * Add one or more members to a sorted set, or update its score if it already exists + * @param key The key + * @return The number of members in the sorted set + */ + def zAddIncr(key: K, score: Double, member: V): Future[Option[Double]] + + /** + * Add one or more members to a sorted set, or update its score if it already exists + * @param key + * @param values + * @return + */ + def zAddIncr(key: K, args: ZAddOptions, score: Double, member: V): Future[Option[Double]] + + /** + * Get the number of members in a sorted set + * @param key The key + * @return The number of members in the sorted set + */ + def zCard(key: K): Future[Long] + + /** + * Count the members in a sorted set with scores within the given values + * @param key The key + * @param range The range of scores + * @return The number of elements in the specified score range + */ + def zCount[T: Numeric](key: K, range: ZRange[T]): Future[Long] + + /** + * Remove and return a member with the lowest score from a sorted set + * @param key The key + * @return The member with the lowest score, or None if the set is empty + */ + def zPopMin(key: K): Future[Option[ScoreWithValue[V]]] + + /** + * Remove and return up to count members with the lowest scores in a sorted set + * @param key The key + * @param count The number of members to pop + * @return The members with the lowest scores + */ def zPopMin(key: K, count: Long): Future[List[ScoreWithValue[V]]] + + /** + * Remove and return a member with the highest score from a sorted set + * @param key The key + * @return The member with the highest score, or None if the set is empty + */ + def zPopMax(key: K): Future[Option[ScoreWithValue[V]]] + + /** + * Remove and return up to count members with the highest scores in a sorted set + * @param key The key + * @param count The number of members to pop + * @return The members with the highest scores + */ def zPopMax(key: K, count: Long): Future[List[ScoreWithValue[V]]] + + /** + * Get the score of a member in a sorted set + * @param key The key + * @param value The value + * @return The score of the member, or None if the member does not exist + */ + def zScore(key: K, value: V): Future[Option[Double]] + + /** + * Return a range of members in a sorted set, by index + * @param key The key + * @param start The start index + * @param stop The stop index + * @return The members in the specified range + */ + def zRange(key: K, start: Long, stop: Long): Future[List[V]] + + /** + * Return a range of members with scores in a sorted set, by index. + * @param key The key + * @param start The start index + * @param stop The stop index + * @return The members with scores in the specified range + */ def zRangeWithScores(key: K, start: Long, stop: Long): Future[List[ScoreWithValue[V]]] + + /** + * Return a range of members in a sorted set, by score + * @param key The key + * @param range The range of scores + * @param limit Optional limit + * @return The members in the specified score range + */ def zRangeByScore[T: Numeric](key: K, range: ZRange[T], limit: Option[RangeLimit]): Future[List[V]] + + /** + * Return a range of members in a sorted set, by score, with scores ordered from high to low + * @param key The key + * @param range The range of scores + * @param limit Optional limit + * @return The members in the specified score range + */ + def zRangeByScoreWithScores[T: Numeric](key: K, range: ZRange[T], limit: Option[RangeLimit] = None): Future[List[ScoreWithValue[V]]] + + /** + * Return a range of members in a sorted set, by score, with scores ordered from high to low + * @param key The key + * @param range The range of scores + * @param limit Optional limit + * @return The members in the specified score range + */ def zRevRangeByScore[T: Numeric](key: K, range: ZRange[T], limit: Option[RangeLimit]): Future[List[V]] + + /** + * Return a range of members in a sorted set, by score, with scores ordered from high to low + * @param key The key + * @param range The range of scores + * @param limit Optional limit + * @return The members in the specified score range + */ + def zRevRangeByScoreWithScores[T: Numeric](key: K, range: ZRange[T], limit: Option[RangeLimit] = None): Future[List[ScoreWithValue[V]]] + + /** + * Increment the score of a member in a sorted set + * @param key The key + * @param amount The amount to increment by + * @param value The value + * @return The new score of the member + */ + def zIncrBy(key: K, amount: Double, value: V): Future[Double] + + /** + * Determine the index of a member in a sorted set + * @param key The key + * @param value The value + * @return The index of the member, or None if the member does not exist + */ + def zRank(key: K, value: V): Future[Option[Long]] + + /** + * Determine the index of a member in a sorted set, with the score + * @param key The key + * @param value The value + * @return The index of the member, or None if the member does not exist + */ + def zRankWithScore(key: K, value: V): Future[Option[ScoreWithValue[Long]]] + + /** + * Determine the index of a member in a sorted set, with the score + * @param key The key + * @param value The value + * @return The index of the member, or None if the member does not exist + */ + def zRevRank(key: K, value: V): Future[Option[Long]] + + /** + * Determine the index of a member in a sorted set, with the score + * @param key The key + * @param value The value + * @return The index of the member, or None if the member does not exist + */ + def zRevRankWithScore(key: K, value: V): Future[Option[ScoreWithValue[Long]]] + + /** + * Scan a sorted set + * @param key The key + * @param cursor The cursor + * @param limit The maximum number of elements to return + * @param matchPattern The pattern to match + * @return The cursor and the values + */ def zScan(key: K, cursor: String = InitialCursor, limit: Option[Long] = None, matchPattern: Option[String] = None): Future[ScanResults[List[ScoreWithValue[V]]]] + + /** + * Get a random member from a sorted set + * @param key The key + * @return A random member from the sorted set, or None if the set is empty + */ + def zRandMember(key: K): Future[Option[V]] + + /** + * Get multiple random members from a sorted set + * @param key The key + * @param count The number of members to get + * @return A list of random members from the sorted set + */ + def zRandMember(key: K, count: Long): Future[List[V]] + + /** + * Get a random member from a sorted set, with the score + * @param key The key + * @return A random member from the sorted set, or None if the set is empty + */ + def zRandMemberWithScores(key: K): Future[Option[ScoreWithValue[V]]] + + /** + * Get multiple random members from a sorted set, with the score + * @param key The key + * @param count The number of members to get + * @return A list of random members from the sorted set + */ + def zRandMemberWithScores(key: K, count: Long): Future[List[ScoreWithValue[V]]] + + /** + * Remove one or more members from a sorted set + * @param key The key + * @param values The values to remove + * @return The number of members removed from the sorted set + */ def zRem(key: K, values: V*): Future[Long] + + /** + * Remove all members in a sorted set with scores between the given values + * @param key The key + * @param start The start score + * @param stop The stop score + * @return The number of members removed from the sorted set + */ def zRemRangeByRank(key: K, start: Long, stop: Long): Future[Long] - def zRemRangeByScore[T: Numeric](key: K, range: ZRange[T]): Future[Long] -} -object RedisSortedSetAsyncCommands { + /** + * Remove all members in a sorted set with scores between the given values + * @param key The key + * @param range The range of scores + * @return The number of members removed from the sorted set + */ + def zRemRangeByScore[T: Numeric](key: K, range: ZRange[T]): Future[Long] - sealed trait ZAddOptions - object ZAddOptions { - case object NX extends ZAddOptions + /** + * Get the score of a member in a sorted set, with the score + * @param key The key + * @param start The start index + * @param stop The stop index + * @return The members with scores in the specified range + */ + def zRevRange(key: K, start: Long, stop: Long): Future[List[V]] - case object XX extends ZAddOptions + /** + * Get the score of a member in a sorted set, with the score + * @param key The key + * @param start The start index + * @param stop The stop index + * @return The members with scores in the specified range + */ + def zRevRangeWithScores(key: K, start: Long, stop: Long): Future[List[ScoreWithValue[V]]] +} - case object LT extends ZAddOptions +// TODO : Implement the following commands: +//bzmpop +//bzpopmin +//bzpopmax +//zaddincr +//zdiff +//zdiffstore +//zdiffWithScores +//zinter +//zintercard +//zinterWithScores +//zinterstore +//zlexcount +//zlexcount +//zmscore +//zmpop +//zrandmemberWithScores +//zrangebylex +//zrangestore +//zrangestorebylex +//zrangestorebyscore +//zremrangebylex +//zrevrangebylex +//zrevrangestore +//zrevrangestorebylex +//zrevrangestorebyscore +//zunion +//zunionWithScores +//zunionstore - case object GT extends ZAddOptions +/** + * Companion object for RedisSortedSetAsyncCommands + */ +object RedisSortedSetAsyncCommands { - case object CH extends ZAddOptions - } + /** + * Options for ZADD. These options are used to modify the behavior of the ZADD command. + * They can be combined to create the desired behavior. + * Note: The GT, LT and NX options are mutually exclusive in Redis. + * @param ch Option Modify the return value from the number of new elements added, to the total number of elements changed. + * @param nx Option to only add new elements + * @param xx Option to only update elements that already exist + * @param lt Option to only add elements with a score less than the given score + * @param gt Option to only add elements with a score greater than the given score + */ + case class ZAddOptions( + ch: Boolean = false, + nx: Boolean = false, + xx: Boolean = false, + lt: Boolean = false, + gt: Boolean = false, + ) final case class ScoreWithValue[V](score: Double, value: V) final case class ZRange[T](start: T, end: T) 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 e47fbe9..b19377e 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,7 +2,6 @@ package com.github.scoquelin.arugula.commands import scala.concurrent.Future -import com.github.scoquelin.arugula.commands.RedisSortedSetAsyncCommands.ZAddOptions.{CH, GT, LT, NX, XX} import com.github.scoquelin.arugula.commands.RedisSortedSetAsyncCommands.{RangeLimit, ScoreWithValue, ZAddOptions, ZRange} import io.lettuce.core.{Limit, Range, ScanArgs, ScoredValue, ZAddArgs} import scala.jdk.CollectionConverters._ @@ -12,30 +11,46 @@ import com.github.scoquelin.arugula.internal.LettuceRedisCommandDelegation private[arugula] trait LettuceRedisSortedSetAsyncCommands[K, V] extends RedisSortedSetAsyncCommands[K, V] with LettuceRedisCommandDelegation[K, V] { import LettuceRedisSortedSetAsyncCommands.toJavaNumberRange - override def zAdd(key: K, args: Option[ZAddOptions], values: ScoreWithValue[V]*): Future[Long] = { - ((args match { - case Some(zAddOption) => zAddOption match { - case NX => Some(ZAddArgs.Builder.nx()) - case XX => Some(ZAddArgs.Builder.xx()) - case LT => Some(ZAddArgs.Builder.lt()) - case GT => Some(ZAddArgs.Builder.gt()) - case CH => Some(ZAddArgs.Builder.ch()) - } - case _ => None - }) match { - case Some(zAddArgs) => - delegateRedisClusterCommandAndLift(_.zadd(key, zAddArgs, values.map(scoreWithValue => ScoredValue.just(scoreWithValue.score, scoreWithValue.value)): _*)) - case None => - delegateRedisClusterCommandAndLift(_.zadd(key, values.map(scoreWithValue => ScoredValue.just(scoreWithValue.score, scoreWithValue.value)): _*)) - }).map(Long2long) + + override def zAdd(key: K, values: ScoreWithValue[V]*): Future[Long] = { + delegateRedisClusterCommandAndLift(_.zadd(key, values.map(scoreWithValue => ScoredValue.just(scoreWithValue.score, scoreWithValue.value)): _*)).map(Long2long) + } + + override def zAdd(key: K, args: ZAddOptions, values: ScoreWithValue[V]*): Future[Long] = { + delegateRedisClusterCommandAndLift(_.zadd(key, LettuceRedisSortedSetAsyncCommands.zAddOptionsToJava(args), values.map(scoreWithValue => ScoredValue.just(scoreWithValue.score, scoreWithValue.value)): _*)).map(Long2long) + } + + override def zAddIncr(key: K, score: Double, member: V): Future[Option[Double]] = + delegateRedisClusterCommandAndLift(_.zaddincr(key, score, member)).map(Option(_).map(Double2double)) + + override def zAddIncr(key: K, args: ZAddOptions, score: Double, member: V): Future[Option[Double]] = { + delegateRedisClusterCommandAndLift(_.zaddincr(key, LettuceRedisSortedSetAsyncCommands.zAddOptionsToJava(args), score, member)).map(Option(_).map(Double2double)) } + override def zCard(key: K): Future[Long] = + delegateRedisClusterCommandAndLift(_.zcard(key)).map(Long2long) + + override def zCount[T: Numeric](key: K, range: ZRange[T]): Future[Long] = + delegateRedisClusterCommandAndLift(_.zcount(key, toJavaNumberRange(range))).map(Long2long) + + override def zPopMin(key: K): Future[Option[ScoreWithValue[V]]] = + delegateRedisClusterCommandAndLift(_.zpopmin(key)).map(Option(_).map(scoredValue => ScoreWithValue(scoredValue.getScore, scoredValue.getValue))) + override def zPopMin(key: K, count: Long): Future[List[ScoreWithValue[V]]] = delegateRedisClusterCommandAndLift(_.zpopmin(key, count)).map(_.asScala.toList.map(scoredValue => ScoreWithValue(scoredValue.getScore, scoredValue.getValue))) + override def zPopMax(key: K): Future[Option[ScoreWithValue[V]]] = + delegateRedisClusterCommandAndLift(_.zpopmax(key)).map(Option(_).map(scoredValue => ScoreWithValue(scoredValue.getScore, scoredValue.getValue))) + override def zPopMax(key: K, count: Long): Future[List[ScoreWithValue[V]]] = delegateRedisClusterCommandAndLift(_.zpopmax(key, count)).map(_.asScala.toList.map(scoredValue => ScoreWithValue(scoredValue.getScore, scoredValue.getValue))) + override def zScore(key: K, value: V): Future[Option[Double]] = + delegateRedisClusterCommandAndLift(_.zscore(key, value)).map(Option(_).map(Double2double)) + + override def zRange(key: K, start: Long, stop: Long): Future[List[V]] = + delegateRedisClusterCommandAndLift(_.zrange(key, start, stop)).map(_.asScala.toList) + override def zRangeByScore[T: Numeric](key: K, range: ZRange[T], limit: Option[RangeLimit] = None): Future[List[V]] = limit match { case Some(rangeLimit) => @@ -44,6 +59,16 @@ private[arugula] trait LettuceRedisSortedSetAsyncCommands[K, V] extends RedisSor delegateRedisClusterCommandAndLift(_.zrangebyscore(key, toJavaNumberRange(range))).map(_.asScala.toList) } + override def zRangeByScoreWithScores[T: Numeric](key: K, range: ZRange[T], limit: Option[RangeLimit] = None): Future[List[ScoreWithValue[V]]] = + limit match { + case Some(rangeLimit) => + delegateRedisClusterCommandAndLift(_.zrangebyscoreWithScores(key, toJavaNumberRange(range), Limit.create(rangeLimit.offset, rangeLimit.count))) + .map(_.asScala.toList.map(scoredValue => ScoreWithValue(scoredValue.getScore, scoredValue.getValue))) + case None => + delegateRedisClusterCommandAndLift(_.zrangebyscoreWithScores(key, toJavaNumberRange(range))) + .map(_.asScala.toList.map(scoredValue => ScoreWithValue(scoredValue.getScore, scoredValue.getValue))) + } + override def zRevRangeByScore[T: Numeric](key: K, range: ZRange[T], limit: Option[RangeLimit] = None): Future[List[V]] = limit match { case Some(rangeLimit) => @@ -52,6 +77,35 @@ private[arugula] trait LettuceRedisSortedSetAsyncCommands[K, V] extends RedisSor delegateRedisClusterCommandAndLift(_.zrevrangebyscore(key, toJavaNumberRange(range))).map(_.asScala.toList) } + override def zRevRangeByScoreWithScores[T: Numeric]( + key: K, + range: ZRange[T], + limit: Option[RangeLimit] = None + ): Future[List[ScoreWithValue[V]]] = + limit match { + case Some(rangeLimit) => + delegateRedisClusterCommandAndLift(_.zrevrangebyscoreWithScores(key, toJavaNumberRange(range), Limit.create(rangeLimit.offset, rangeLimit.count))) + .map(_.asScala.toList.map(scoredValue => ScoreWithValue(scoredValue.getScore, scoredValue.getValue))) + case None => + delegateRedisClusterCommandAndLift(_.zrevrangebyscoreWithScores(key, toJavaNumberRange(range))) + .map(_.asScala.toList.map(scoredValue => ScoreWithValue(scoredValue.getScore, scoredValue.getValue))) + } + + override def zIncrBy(key: K, amount: Double, value: V): Future[Double] = + delegateRedisClusterCommandAndLift(_.zincrby(key, amount, value)).map(Double2double) + + override def zRank(key: K, value: V): Future[Option[Long]] = + delegateRedisClusterCommandAndLift(_.zrank(key, value)).map(Option(_).map(Long2long)) + + override def zRankWithScore(key: K, value: V): Future[Option[ScoreWithValue[Long]]] = + delegateRedisClusterCommandAndLift(_.zrankWithScore(key, value)).map(Option(_).map(scoredValue => ScoreWithValue[Long](scoredValue.getScore, scoredValue.getValue))) + + override def zRevRank(key: K, value: V): Future[Option[Long]] = + delegateRedisClusterCommandAndLift(_.zrevrank(key, value)).map(Option(_).map(Long2long)) + + override def zRevRankWithScore(key: K, value: V): Future[Option[ScoreWithValue[Long]]] = + delegateRedisClusterCommandAndLift(_.zrevrankWithScore(key, value)).map(Option(_).map(scoredValue => ScoreWithValue(scoredValue.getScore, scoredValue.getValue))) + override def zRangeWithScores(key: K, start: Long, stop: Long): Future[List[ScoreWithValue[V]]] = delegateRedisClusterCommandAndLift(_.zrangeWithScores(key, start, stop)) .map(_.asScala.toList.map(scoredValue => ScoreWithValue(scoredValue.getScore, scoredValue.getValue))) @@ -88,6 +142,18 @@ private[arugula] trait LettuceRedisSortedSetAsyncCommands[K, V] extends RedisSor } } + override def zRandMember(key: K): Future[Option[V]] = + delegateRedisClusterCommandAndLift(_.zrandmember(key)).map(Option(_)) + + override def zRandMember(key: K, count: Long): Future[List[V]] = + delegateRedisClusterCommandAndLift(_.zrandmember(key, count)).map(_.asScala.toList) + + override def zRandMemberWithScores(key: K): Future[Option[ScoreWithValue[V]]] = + delegateRedisClusterCommandAndLift(_.zrandmemberWithScores(key)).map(Option(_).map(scoredValue => ScoreWithValue(scoredValue.getScore, scoredValue.getValue))) + + override def zRandMemberWithScores(key: K, count: Long): Future[List[ScoreWithValue[V]]] = + delegateRedisClusterCommandAndLift(_.zrandmemberWithScores(key, count)).map(_.asScala.toList.map(scoredValue => ScoreWithValue(scoredValue.getScore, scoredValue.getValue))) + override def zRem(key: K, values: V*): Future[Long] = delegateRedisClusterCommandAndLift(_.zrem(key, values: _*)).map(Long2long) @@ -97,6 +163,13 @@ private[arugula] trait LettuceRedisSortedSetAsyncCommands[K, V] extends RedisSor override def zRemRangeByScore[T: Numeric](key: K, range: ZRange[T]): Future[Long] = delegateRedisClusterCommandAndLift(_.zremrangebyscore(key, toJavaNumberRange(range))).map(Long2long) + override def zRevRange(key: K, start: Long, stop: Long): Future[List[V]] = + delegateRedisClusterCommandAndLift(_.zrevrange(key, start, stop)).map(_.asScala.toList) + + override def zRevRangeWithScores(key: K, start: Long, stop: Long): Future[List[ScoreWithValue[V]]] = + delegateRedisClusterCommandAndLift(_.zrevrangeWithScores(key, start, stop)) + .map(_.asScala.toList.map(scoredValue => ScoreWithValue(scoredValue.getScore, scoredValue.getValue))) + } private[this] object LettuceRedisSortedSetAsyncCommands{ @@ -111,4 +184,14 @@ private[this] object LettuceRedisSortedSetAsyncCommands{ } Range.create(toJavaNumber(range.start), toJavaNumber(range.end)) } + + private[commands] def zAddOptionsToJava(options: ZAddOptions): ZAddArgs = { + val args = new ZAddArgs() + if (options.nx) args.nx() + if (options.xx) args.xx() + if(options.gt) args.gt() + if(options.lt) args.lt() + if (options.ch) args.ch() + args + } } \ No newline at end of file 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 b8dc36e..ef9f749 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 @@ -418,18 +418,30 @@ class RedisCommandsIntegrationSpec extends BaseRedisCommandsIntegrationSpec with val key = randomKey("sorted-set") for { - zAdd <- client.zAdd(key = key, args = None, ScoreWithValue(1, "one")) + zAdd <- client.zAdd(key = key, ScoreWithValue(1, "one")) _ <- zAdd shouldBe 1L - zAddWithNx <- client.zAdd(key = key, args = Some(ZAddOptions.NX), ScoreWithValue(1, "one")) + zAddWithNx <- client.zAdd(key = key, args = ZAddOptions(nx=true), ScoreWithValue(1, "one")) _ <- zAddWithNx shouldBe 0L - zAddNewValueWithNx <- client.zAdd(key = key, args = Some(ZAddOptions.NX), ScoreWithValue(2, "two"), ScoreWithValue(3, "three"), ScoreWithValue(4, "four"), ScoreWithValue(5, "five")) + zAddNewValueWithNx <- client.zAdd(key = key, args = ZAddOptions(nx=true), ScoreWithValue(2, "two"), ScoreWithValue(3, "three"), ScoreWithValue(4, "four"), ScoreWithValue(5, "five")) _ <- zAddNewValueWithNx shouldBe 4L + zRange <- client.zRange(key, 0, -1) + _ <- zRange shouldBe List("one", "two", "three", "four", "five") rangeWithScores <- client.zRangeWithScores(key, 0, 1) _ <- rangeWithScores.shouldBe(List(ScoreWithValue(1, "one"), ScoreWithValue(2, "two"))) rangeByScore <- client.zRangeByScore(key, ZRange(0, 2), Some(RangeLimit(0, 2))) _ <- rangeByScore.shouldBe(List("one", "two")) + rangeByScoreWithScores <- client.zRangeByScoreWithScores(key, ZRange(0, 2), Some(RangeLimit(0, 2))) + _ <- rangeByScoreWithScores.shouldBe(List(ScoreWithValue(1, "one"), ScoreWithValue(2, "two"))) + revRange <- client.zRevRange(key, 0, -1) + _ <- revRange shouldBe List("five", "four", "three", "two", "one") revRangeByScore <- client.zRevRangeByScore(key, ZRange(0, 2), Some(RangeLimit(0, 2))) _ <- revRangeByScore.shouldBe(List("two", "one")) + revRangeWithScores <- client.zRevRangeWithScores(key, 0, -1) + _ <- revRangeWithScores shouldBe List(ScoreWithValue(5, "five"), ScoreWithValue(4, "four"), ScoreWithValue(3, "three"), ScoreWithValue(2, "two"), ScoreWithValue(1, "one")) + zCard <- client.zCard(key) + _ <- zCard shouldBe 5L + zCount <- client.zCount(key, ZRange(0, 2)) + _ <- zCount shouldBe 2L zScan <- client.zScan(key) _ <- zScan.finished shouldBe true _ <- zScan.values shouldBe List(ScoreWithValue(1, "one"), ScoreWithValue(2, "two"), ScoreWithValue(3, "three"), ScoreWithValue(4, "four"), ScoreWithValue(5, "five")) @@ -452,12 +464,45 @@ class RedisCommandsIntegrationSpec extends BaseRedisCommandsIntegrationSpec with _ <- zPopMax.headOption shouldBe Some(ScoreWithValue(5, "five")) zRem <- client.zRem(key, "four") _ <- zRem shouldBe 1L - endState <- client.zRangeWithScores(key, 0, -1) _ <- endState.isEmpty shouldBe true } yield succeed } } + + "support random key operations" in { + withRedisSingleNodeAndCluster(RedisCodec.Utf8WithValueAsStringCodec) { client => + val key = randomKey("sorted-set-random") + for { + _ <- client.zAdd(key, ScoreWithValue(1, "one"), ScoreWithValue(2, "two"), ScoreWithValue(3, "three"), ScoreWithValue(4, "four"), ScoreWithValue(5, "five")) + randomKey <- client.zRandMember(key) + _ <- randomKey.isDefined shouldBe true + randomKeys <- client.zRandMember(key, 3) + _ <- randomKeys.size shouldBe 3 + randomKeyWithValue <- client.zRandMemberWithScores(key) + _ <- randomKeyWithValue.isDefined shouldBe true + randomKeysWithValues <- client.zRandMemberWithScores(key, 3) + _ <- randomKeysWithValues.size shouldBe 3 + } yield succeed + } + } + + "support increment operations" in { + withRedisSingleNodeAndCluster(RedisCodec.Utf8WithValueAsStringCodec) { client => + val key = randomKey("sorted-set-incr") + for { + _ <- client.zAdd(key, ScoreWithValue(1, "one"), ScoreWithValue(2, "two"), ScoreWithValue(3, "three")) + incrResult <- client.zIncrBy(key, 2, "two") + _ <- incrResult shouldBe 4.0 + incrResult <- client.zIncrBy(key, 2, "four") + _ <- incrResult shouldBe 2.0 + getResult <- client.zScore(key, "four") + _ <- getResult shouldBe Some(2.0) + zRankResult <- client.zRank(key, "four") + _ <- zRankResult shouldBe Some(1L) + } yield succeed + } + } } "leveraging RedisHashAsyncCommands" should { diff --git a/modules/tests/test/src/test/scala/com/github/scoquelin/arugula/LettuceRedisPipelineAsyncCommandsSpec.scala b/modules/tests/test/src/test/scala/com/github/scoquelin/arugula/LettuceRedisPipelineAsyncCommandsSpec.scala new file mode 100644 index 0000000..872254f --- /dev/null +++ b/modules/tests/test/src/test/scala/com/github/scoquelin/arugula/LettuceRedisPipelineAsyncCommandsSpec.scala @@ -0,0 +1,43 @@ +package com.github.scoquelin.arugula + +import scala.concurrent.Future +import scala.concurrent.duration.DurationInt + +import io.lettuce.core.RedisFuture +import org.mockito.Mockito.{verify, when} +import org.scalatest.matchers.must.Matchers +import org.scalatest.{FutureOutcome, wordspec} + +class LettuceRedisPipelineAsyncCommandsSpec extends wordspec.FixtureAsyncWordSpec with Matchers { + + override type FixtureParam = LettuceRedisCommandsClientFixture.TestContext + + override def withFixture(test: OneArgAsyncTest): FutureOutcome = + withFixture(test.toNoArgAsyncTest(new LettuceRedisCommandsClientFixture.TestContext)) + + "LettuceRedisPipelineAsyncCommands" should { + + "send a batch of commands WITH NO transaction guarantees" in { testContext => + import testContext._ + + val mockHsetRedisFuture: RedisFuture[java.lang.Boolean] = mockRedisFutureToReturn(true) + when(lettuceAsyncCommands.hset("userKey", "sessionId", "token")).thenReturn(mockHsetRedisFuture) + + val mockExpireRedisFuture: RedisFuture[java.lang.Boolean] = mockRedisFutureToReturn(true) + when(lettuceAsyncCommands.expire("userKey", 24.hours.toSeconds)).thenReturn(mockExpireRedisFuture) + + val commands: RedisCommandsClient[String, String] => List[Future[Any]] = availableCommands => List( + availableCommands.hSet("userKey", "sessionId", "token"), + availableCommands.expire("userKey", 24.hours) + ) + + testClass.pipeline(commands).map { results => + results mustBe Some(List(true, true)) + verify(lettuceAsyncCommands).hset("userKey", "sessionId", "token") + verify(lettuceAsyncCommands).expire("userKey", 24.hours.toSeconds) + 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 40b8e1a..20d054d 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 @@ -1,9 +1,7 @@ package com.github.scoquelin.arugula -import scala.concurrent.Future -import scala.concurrent.duration.DurationInt -import com.github.scoquelin.arugula.commands.RedisSortedSetAsyncCommands.{RangeLimit, ScoreWithValue, ZRange} +import com.github.scoquelin.arugula.commands.RedisSortedSetAsyncCommands.{RangeLimit, ScoreWithValue, ZAddOptions, ZRange} import io.lettuce.core.{RedisFuture, ScoredValue, ScoredValueScanCursor} import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers.{any, eq => meq} @@ -27,13 +25,103 @@ class LettuceRedisSortedSetAsyncCommandsSpec extends wordspec.FixtureAsyncWordSp when(lettuceAsyncCommands.zadd("key", ScoredValue.just(1, "one"))).thenReturn(mockRedisFuture) - testClass.zAdd("key", None, ScoreWithValue(1, "one")).map { result => + testClass.zAdd("key", ScoreWithValue(1, "one")).map { result => result mustBe expectedValue verify(lettuceAsyncCommands).zadd("key", ScoredValue.just(1, "one")) succeed } } + "delegate ZADD command with multiple values to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue = 2L + val mockRedisFuture: RedisFuture[java.lang.Long] = mockRedisFutureToReturn(expectedValue) + + when(lettuceAsyncCommands.zadd("key", ScoredValue.just(1, "one"), ScoredValue.just(2, "two"))).thenReturn(mockRedisFuture) + + testClass.zAdd("key", ScoreWithValue(1, "one"), ScoreWithValue(2, "two")).map { result => + result mustBe expectedValue + verify(lettuceAsyncCommands).zadd("key", ScoredValue.just(1, "one"), ScoredValue.just(2, "two")) + succeed + } + } + + "delegate ZADD command with NX option to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue = 1L + val mockRedisFuture: RedisFuture[java.lang.Long] = mockRedisFutureToReturn(expectedValue) + + when(lettuceAsyncCommands.zadd(any[String], any[io.lettuce.core.ZAddArgs], any[ScoredValue[String]])).thenReturn(mockRedisFuture) + + testClass.zAdd("key", ZAddOptions(nx=true), ScoreWithValue(1, "one")).map { result => + result mustBe expectedValue + verify(lettuceAsyncCommands).zadd(meq("key"), any[io.lettuce.core.ZAddArgs], meq(ScoredValue.just(1, "one"))) + succeed + } + } + + "delegate ZADDINCR command to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue = 1.0 + val mockRedisFuture: RedisFuture[java.lang.Double] = mockRedisFutureToReturn(expectedValue) + + when(lettuceAsyncCommands.zaddincr("key", 1.0, "one")).thenReturn(mockRedisFuture) + + testClass.zAddIncr("key", 1.0, "one").map { result => + result mustBe Some(expectedValue) + verify(lettuceAsyncCommands).zaddincr("key", 1.0, "one") + succeed + } + } + + "delegate ZADDINCR command with args to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue = 2.0 + val mockRedisFuture: RedisFuture[java.lang.Double] = mockRedisFutureToReturn(expectedValue) + + when(lettuceAsyncCommands.zaddincr(any[String], any[io.lettuce.core.ZAddArgs], any[Double], any[String])).thenReturn(mockRedisFuture) + + testClass.zAddIncr("key", ZAddOptions(nx=true), 1.0, "one").map { result => + result mustBe Some(expectedValue) + verify(lettuceAsyncCommands).zaddincr(meq("key"), any[io.lettuce.core.ZAddArgs], meq(1.0), meq("one")) + succeed + } + } + + "delegate ZCARD command to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue = 1L + val mockRedisFuture: RedisFuture[java.lang.Long] = mockRedisFutureToReturn(expectedValue) + + when(lettuceAsyncCommands.zcard("key")).thenReturn(mockRedisFuture) + + testClass.zCard("key").map { result => + result mustBe expectedValue + verify(lettuceAsyncCommands).zcard("key") + succeed + } + } + + "delegate ZCOUNT command to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue = 1L + val mockRedisFuture: RedisFuture[java.lang.Long] = mockRedisFutureToReturn(expectedValue) + + when(lettuceAsyncCommands.zcount("key", io.lettuce.core.Range.create(0, 1))).thenReturn(mockRedisFuture) + + testClass.zCount("key", ZRange(0, 1)).map { result => + result mustBe expectedValue + verify(lettuceAsyncCommands).zcount("key", io.lettuce.core.Range.create(0, 1)) + succeed + } + } + "delegate ZPOPMIN command to Lettuce and lift result into a Future" in { testContext => import testContext._ @@ -82,20 +170,19 @@ class LettuceRedisSortedSetAsyncCommandsSpec extends wordspec.FixtureAsyncWordSp } } - "delegate ZRANGEBYSCORE command with a limit to Lettuce and lift result into a Future" in { testContext => + "delegate ZRANGE command to Lettuce and lift result into a Future" in { testContext => import testContext._ val expectedValue: java.util.List[String] = new java.util.ArrayList[String] expectedValue.add(0, "one") val mockRedisFuture: RedisFuture[java.util.List[String]] = mockRedisFutureToReturn(expectedValue) - when(lettuceAsyncCommands.zrangebyscore(meq("key"), any[io.lettuce.core.Range[java.lang.Number]](), any[io.lettuce.core.Limit]())).thenReturn(mockRedisFuture) + when(lettuceAsyncCommands.zrange("key", 0, 0)).thenReturn(mockRedisFuture) - testClass.zRangeByScore("key", ZRange(Double.NegativeInfinity, Double.PositiveInfinity), Some(RangeLimit(0, 10))).map { result => + testClass.zRange("key", 0, 0).map { result => result mustBe List("one") - val limitArgumentCaptor: ArgumentCaptor[io.lettuce.core.Limit] = ArgumentCaptor.forClass(classOf[io.lettuce.core.Limit]) //using captor to assert Limit offset/count as it seems equals is not implemented - verify(lettuceAsyncCommands).zrangebyscore(meq("key"), meq(io.lettuce.core.Range.create(Double.NegativeInfinity: Number, Double.PositiveInfinity: Number)), limitArgumentCaptor.capture()) - (limitArgumentCaptor.getValue.getOffset, limitArgumentCaptor.getValue.getCount) mustBe (0, 10) + verify(lettuceAsyncCommands).zrange("key", 0, 0) + succeed } } @@ -115,6 +202,51 @@ class LettuceRedisSortedSetAsyncCommandsSpec extends wordspec.FixtureAsyncWordSp } } + "delegate ZINCRBY command to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue = 2.0 + val mockRedisFuture: RedisFuture[java.lang.Double] = mockRedisFutureToReturn(expectedValue) + + when(lettuceAsyncCommands.zincrby("key", 1.0, "one")).thenReturn(mockRedisFuture) + + testClass.zIncrBy("key", 1.0, "one").map { result => + result mustBe expectedValue + verify(lettuceAsyncCommands).zincrby("key", 1.0, "one") + succeed + } + } + + "delegate ZRANK command to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue = 0L + val mockRedisFuture: RedisFuture[java.lang.Long] = mockRedisFutureToReturn(expectedValue) + + when(lettuceAsyncCommands.zrank("key", "one")).thenReturn(mockRedisFuture) + + testClass.zRank("key", "one").map { result => + result mustBe Some(expectedValue) + verify(lettuceAsyncCommands).zrank("key", "one") + succeed + } + } + + "delegate ZRANK WITHSCORE command to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue = ScoreWithValue(0.0, 1L) + val mockRedisFuture: RedisFuture[ScoredValue[java.lang.Long]] = mockRedisFutureToReturn(ScoredValue.just(0.0, java.lang.Long.valueOf(1))) + + when(lettuceAsyncCommands.zrankWithScore("key", "one")).thenReturn(mockRedisFuture) + + testClass.zRankWithScore("key", "one").map { result => + result mustBe Some(expectedValue) + verify(lettuceAsyncCommands).zrankWithScore("key", "one") + succeed + } + } + "delegate ZSCAN command to Lettuce and lift result into a Future" in { testContext => import testContext._ val scoredValues: java.util.List[ScoredValue[String]] = new java.util.ArrayList[ScoredValue[String]] @@ -164,6 +296,150 @@ class LettuceRedisSortedSetAsyncCommandsSpec extends wordspec.FixtureAsyncWordSp } } + "delegate ZSCORE command to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue = 1.0 + val mockRedisFuture: RedisFuture[java.lang.Double] = mockRedisFutureToReturn(expectedValue) + + when(lettuceAsyncCommands.zscore("key", "one")).thenReturn(mockRedisFuture) + + testClass.zScore("key", "one").map { result => + result mustBe Some(expectedValue) + verify(lettuceAsyncCommands).zscore("key", "one") + succeed + } + } + + "delegate ZREVRANGE command to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue: java.util.List[String] = new java.util.ArrayList[String] + expectedValue.add(0, "one") + val mockRedisFuture: RedisFuture[java.util.List[String]] = mockRedisFutureToReturn(expectedValue) + + when(lettuceAsyncCommands.zrevrange("key", 0, 0)).thenReturn(mockRedisFuture) + + testClass.zRevRange("key", 0, 0).map { result => + result mustBe List("one") + verify(lettuceAsyncCommands).zrevrange("key", 0, 0) + succeed + } + } + + "delegate ZREVRANGE WITHSCORES command to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue: java.util.List[ScoredValue[String]] = new java.util.ArrayList[ScoredValue[String]] + expectedValue.add(ScoredValue.just(1, "one")) + val mockRedisFuture: RedisFuture[java.util.List[ScoredValue[String]]] = mockRedisFutureToReturn(expectedValue) + + when(lettuceAsyncCommands.zrevrangeWithScores("key", 0, 0)).thenReturn(mockRedisFuture) + + testClass.zRevRangeWithScores("key", 0, 0).map { result => + result mustBe List(ScoreWithValue(1, "one")) + verify(lettuceAsyncCommands).zrevrangeWithScores("key", 0, 0) + succeed + } + } + + "delegate ZREVRANGEBYSCORE command to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue: java.util.List[String] = new java.util.ArrayList[String] + expectedValue.add(0, "one") + val mockRedisFuture: RedisFuture[java.util.List[String]] = mockRedisFutureToReturn(expectedValue) + + when(lettuceAsyncCommands.zrevrangebyscore(meq("key"), any[io.lettuce.core.Range[java.lang.Number]]())).thenReturn(mockRedisFuture) + + testClass.zRevRangeByScore("key", ZRange(Double.NegativeInfinity, Double.PositiveInfinity)).map { result => + result mustBe List("one") + verify(lettuceAsyncCommands).zrevrangebyscore("key", io.lettuce.core.Range.create(Double.NegativeInfinity, Double.PositiveInfinity)) + succeed + } + } + + "delegate ZREVRANGEBYSCORE command with a limit to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue: java.util.List[String] = new java.util.ArrayList[String] + expectedValue.add(0, "one") + val mockRedisFuture: RedisFuture[java.util.List[String]] = mockRedisFutureToReturn(expectedValue) + + when(lettuceAsyncCommands.zrevrangebyscore(meq("key"), any[io.lettuce.core.Range[java.lang.Number]](), any[io.lettuce.core.Limit]())).thenReturn(mockRedisFuture) + + testClass.zRevRangeByScore("key", ZRange(Double.NegativeInfinity, Double.PositiveInfinity), Some(RangeLimit(0, 10))).map { result => + result mustBe List("one") + val limitArgumentCaptor: ArgumentCaptor[io.lettuce.core.Limit] = ArgumentCaptor.forClass(classOf[io.lettuce.core.Limit]) //using captor to assert Limit offset/count as it seems equals is not implemented + verify(lettuceAsyncCommands).zrevrangebyscore(meq("key"), meq(io.lettuce.core.Range.create(Double.NegativeInfinity: Number, Double.PositiveInfinity: Number)), limitArgumentCaptor.capture()) + (limitArgumentCaptor.getValue.getOffset, limitArgumentCaptor.getValue.getCount) mustBe (0, 10) + } + } + + "delegate ZREVRANGEBYSCORE WITHSCORES command to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue: java.util.List[ScoredValue[String]] = new java.util.ArrayList[ScoredValue[String]] + expectedValue.add(ScoredValue.just(1, "one")) + val mockRedisFuture: RedisFuture[java.util.List[ScoredValue[String]]] = mockRedisFutureToReturn(expectedValue) + + when(lettuceAsyncCommands.zrevrangebyscoreWithScores("key", io.lettuce.core.Range.create(0, 1))).thenReturn(mockRedisFuture) + + testClass.zRevRangeByScoreWithScores("key", ZRange(0, 1)).map { result => + result mustBe List(ScoreWithValue(1, "one")) + verify(lettuceAsyncCommands).zrevrangebyscoreWithScores("key", io.lettuce.core.Range.create(0, 1)) + succeed + } + } + + "delegate ZRANGEBYSCORE command with a limit to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue: java.util.List[String] = new java.util.ArrayList[String] + expectedValue.add(0, "one") + val mockRedisFuture: RedisFuture[java.util.List[String]] = mockRedisFutureToReturn(expectedValue) + + when(lettuceAsyncCommands.zrangebyscore(meq("key"), any[io.lettuce.core.Range[java.lang.Number]](), any[io.lettuce.core.Limit]())).thenReturn(mockRedisFuture) + + testClass.zRangeByScore("key", ZRange(Double.NegativeInfinity, Double.PositiveInfinity), Some(RangeLimit(0, 10))).map { result => + result mustBe List("one") + val limitArgumentCaptor: ArgumentCaptor[io.lettuce.core.Limit] = ArgumentCaptor.forClass(classOf[io.lettuce.core.Limit]) //using captor to assert Limit offset/count as it seems equals is not implemented + verify(lettuceAsyncCommands).zrangebyscore(meq("key"), meq(io.lettuce.core.Range.create(Double.NegativeInfinity: Number, Double.PositiveInfinity: Number)), limitArgumentCaptor.capture()) + (limitArgumentCaptor.getValue.getOffset, limitArgumentCaptor.getValue.getCount) mustBe (0, 10) + } + } + + "delegate ZRANGEBYSCORE WITHSCORES command to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue: java.util.List[ScoredValue[String]] = new java.util.ArrayList[ScoredValue[String]] + expectedValue.add(ScoredValue.just(1, "one")) + val mockRedisFuture: RedisFuture[java.util.List[ScoredValue[String]]] = mockRedisFutureToReturn(expectedValue) + + when(lettuceAsyncCommands.zrangebyscoreWithScores(meq("key"), any[io.lettuce.core.Range[java.lang.Number]]())).thenReturn(mockRedisFuture) + + testClass.zRangeByScoreWithScores("key", ZRange(Double.NegativeInfinity, Double.PositiveInfinity)).map { result => + result mustBe List(ScoreWithValue(1, "one")) + verify(lettuceAsyncCommands).zrangebyscoreWithScores("key", io.lettuce.core.Range.create(Double.NegativeInfinity, Double.PositiveInfinity)) + succeed + } + } + + "delegate ZREVRANK command to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue = 0L + val mockRedisFuture: RedisFuture[java.lang.Long] = mockRedisFutureToReturn(expectedValue) + + when(lettuceAsyncCommands.zrevrank("key", "one")).thenReturn(mockRedisFuture) + + testClass.zRevRank("key", "one").map { result => + result mustBe Some(expectedValue) + verify(lettuceAsyncCommands).zrevrank("key", "one") + succeed + } + } + "delegate ZREM command to Lettuce and lift result into a Future" in { testContext => import testContext._ @@ -209,27 +485,66 @@ class LettuceRedisSortedSetAsyncCommandsSpec extends wordspec.FixtureAsyncWordSp } } - "send a batch of commands WITH NO transaction guarantees" in { testContext => + "delegate ZRANDMEMBER command to Lettuce and lift result into a Future" in { testContext => import testContext._ - val mockHsetRedisFuture: RedisFuture[java.lang.Boolean] = mockRedisFutureToReturn(true) - when(lettuceAsyncCommands.hset("userKey", "sessionId", "token")).thenReturn(mockHsetRedisFuture) + val expectedValue = "one" + val mockRedisFuture: RedisFuture[String] = mockRedisFutureToReturn(expectedValue) - val mockExpireRedisFuture: RedisFuture[java.lang.Boolean] = mockRedisFutureToReturn(true) - when(lettuceAsyncCommands.expire("userKey", 24.hours.toSeconds)).thenReturn(mockExpireRedisFuture) + when(lettuceAsyncCommands.zrandmember("key")).thenReturn(mockRedisFuture) - val commands: RedisCommandsClient[String, String] => List[Future[Any]] = availableCommands => List( - availableCommands.hSet("userKey", "sessionId", "token"), - availableCommands.expire("userKey", 24.hours) - ) + testClass.zRandMember("key").map { result => + result mustBe Some(expectedValue) + verify(lettuceAsyncCommands).zrandmember("key") + succeed + } + } - testClass.pipeline(commands).map { results => - results mustBe Some(List(true, true)) - verify(lettuceAsyncCommands).hset("userKey", "sessionId", "token") - verify(lettuceAsyncCommands).expire("userKey", 24.hours.toSeconds) + "delegate ZRANDMEMBER command with count to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue: java.util.List[String] = new java.util.ArrayList[String] + expectedValue.add(0, "one") + val mockRedisFuture: RedisFuture[java.util.List[String]] = mockRedisFutureToReturn(expectedValue) + + when(lettuceAsyncCommands.zrandmember("key", 1)).thenReturn(mockRedisFuture) + + testClass.zRandMember("key", 1).map { result => + result mustBe List("one") + verify(lettuceAsyncCommands).zrandmember("key", 1) succeed } } - } + "delegate ZRANDMEMBER WITHSCORES command to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue = ScoredValue.just(1, "one") + val mockRedisFuture: RedisFuture[ScoredValue[String]] = mockRedisFutureToReturn(expectedValue) + + when(lettuceAsyncCommands.zrandmemberWithScores("key")).thenReturn(mockRedisFuture) + + testClass.zRandMemberWithScores("key").map { result => + result mustBe Some(ScoreWithValue(1, "one")) + verify(lettuceAsyncCommands).zrandmemberWithScores("key") + succeed + } + } + + "delegate ZRANDMEMBER WITHSCORES command with count to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue: java.util.List[ScoredValue[String]] = new java.util.ArrayList[ScoredValue[String]] + expectedValue.add(ScoredValue.just(1, "one")) + val mockRedisFuture: RedisFuture[java.util.List[ScoredValue[String]]] = mockRedisFutureToReturn(expectedValue) + + when(lettuceAsyncCommands.zrandmemberWithScores("key", 1)).thenReturn(mockRedisFuture) + + testClass.zRandMemberWithScores("key", 1).map { result => + result mustBe List(ScoreWithValue(1, "one")) + verify(lettuceAsyncCommands).zrandmemberWithScores("key", 1) + succeed + } + } + } }