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..d6b423c 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,36 +5,400 @@ 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] + + /** + * Remove and return the member with the lowest score from one or more sorted sets + * @param timeout The timeout + * @param keys The keys + * @return The member with the lowest score, or None if the sets are empty + */ + def bzPopMin(timeout: Long, keys: K*): Future[Option[ScoreWithKeyValue[K,V]]] + + /** + * Remove and return the member with the highest score from one or more sorted sets + * @param timeout The timeout + * @param keys The keys + * @return The member with the highest score, or None if the sets are empty + */ + def bzPopMin(timeout: Double, keys: K*): Future[Option[ScoreWithKeyValue[K,V]]] + + /** + * Remove and return the member with the highest score from one or more sorted sets + * @param timeout The timeout + * @param keys The keys + * @return The member with the highest score, or None if the sets are empty + */ + def bzPopMax(timeout: Long, keys: K*): Future[Option[ScoreWithKeyValue[K,V]]] + + /** + * Remove and return the member with the highest score from one or more sorted sets + * @param timeout The timeout + * @param keys The keys + * @return The member with the highest score, or None if the sets are empty + */ + def bzPopMax(timeout: Double, keys: K*): Future[Option[ScoreWithKeyValue[K,V]]] + + /** + * 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] = zAdd(key, Set(args), values: _*) + + /** + * 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: Set[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 The key + * @param args The arguments + * @param values The values to add + * @return The number of elements added to the sorted set + */ + def zAddIncr(key: K, args: ZAddOptions, score: Double, member: V): Future[Option[Double]] = zAddIncr(key, Set(args), score, member) + + /** + * 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 zAddIncr(key: K, args: Set[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 scores ordered from high to low. + * @param key The key. + * @param value the member type: value. + * @return Long integer-reply the rank of the element as an integer-reply, with the scores ordered from high to low + * 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] + + /** + * 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] + + /** + * 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]] + + /** + * 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]]] } +// TODO : Implement the following commands: +//bzmpop +//zdiff +//zdiffstore +//zdiffWithScores +//zinter +//zintercard +//zinterWithScores +//zinterstore +//zlexcount +//zlexcount +//zmscore +//zmpop +//zrandmemberWithScores +//zrangebylex +//zrangestore +//zrangestorebylex +//zrangestorebyscore +//zremrangebylex +//zrevrangebylex +//zrevrangestore +//zrevrangestorebylex +//zrevrangestorebyscore +//zunion +//zunionWithScores +//zunionstore + +/** + * Companion object for RedisSortedSetAsyncCommands + */ object RedisSortedSetAsyncCommands { sealed trait ZAddOptions object ZAddOptions { + + /** + * Takes a varargs of ZAddOptions and returns a Set of ZAddOptions. + * Useful for passing multiple options to a command. + * @param options The options + * @return The set of options + */ + def apply(options: ZAddOptions*): Set[ZAddOptions] = options.toSet + + /** + * Only add new elements + */ case object NX extends ZAddOptions + /** + * Only update elements that already exist + */ case object XX extends ZAddOptions + /** + * Only update elements that already exist and return the new score + */ case object LT extends ZAddOptions + /** + * Only add new elements and return the new score + */ case object GT extends ZAddOptions + /** + * Only update elements that already exist and return the new score + */ case object CH extends ZAddOptions } final case class ScoreWithValue[V](score: Double, value: V) + final case class ScoreWithKeyValue[K, V](score: Double, key: K, value: V) final case class ZRange[T](start: T, end: T) final case class RangeLimit(offset: Long, count: Long) } \ 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 e47fbe9..a1d6284 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,8 +2,7 @@ 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 com.github.scoquelin.arugula.commands.RedisSortedSetAsyncCommands.{RangeLimit, ScoreWithKeyValue, ScoreWithValue, ZAddOptions, ZRange} import io.lettuce.core.{Limit, Range, ScanArgs, ScoredValue, ZAddArgs} import scala.jdk.CollectionConverters._ @@ -11,30 +10,87 @@ import com.github.scoquelin.arugula.commands.RedisBaseAsyncCommands.{InitialCurs 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()) - } + + override def bzPopMin(timeout: Long, keys: K*): Future[Option[ScoreWithKeyValue[K, V]]] = + delegateRedisClusterCommandAndLift(_.bzpopmin(timeout, keys: _*)).map { + case null => None + case scoredValue if scoredValue.hasValue => Some(ScoreWithKeyValue(scoredValue.getValue.getScore, scoredValue.getKey, scoredValue.getValue.getValue)) 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 bzPopMin(timeout: Double, keys: K*): Future[Option[ScoreWithKeyValue[K, V]]] = + delegateRedisClusterCommandAndLift(_.bzpopmin(timeout, keys: _*)).map { + case null => None + case scoredValue if scoredValue.hasValue => Some(ScoreWithKeyValue(scoredValue.getValue.getScore, scoredValue.getKey, scoredValue.getValue.getValue)) + case _ => None + } + + override def bzPopMax(timeout: Long, keys: K*): Future[Option[ScoreWithKeyValue[K, V]]] = + delegateRedisClusterCommandAndLift(_.bzpopmax(timeout, keys: _*)).map { + case null => None + case scoredValue if scoredValue.hasValue => Some(ScoreWithKeyValue(scoredValue.getValue.getScore, scoredValue.getKey, scoredValue.getValue.getValue)) + case _ => None + } + + override def bzPopMax(timeout: Double, keys: K*): Future[Option[ScoreWithKeyValue[K, V]]] = + delegateRedisClusterCommandAndLift(_.bzpopmax(timeout, keys: _*)).map { + case null => None + case scoredValue if scoredValue.hasValue => Some(ScoreWithKeyValue(scoredValue.getValue.getScore, scoredValue.getKey, scoredValue.getValue.getValue)) + case _ => None + } + + 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: Set[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: Set[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 { + case null => None + case scoredValue if scoredValue.hasValue => Some(ScoreWithValue(scoredValue.getScore, scoredValue.getValue)) + case _ => None + } + 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))) + delegateRedisClusterCommandAndLift(_.zpopmin(key, count)).map(_.asScala.toList.collect { + case scoredValue if scoredValue.hasValue => ScoreWithValue(scoredValue.getScore, scoredValue.getValue) + }) + + override def zPopMax(key: K): Future[Option[ScoreWithValue[V]]] = + delegateRedisClusterCommandAndLift(_.zpopmax(key)).map { + case null => None + case scoredValue if scoredValue.hasValue => Some(ScoreWithValue(scoredValue.getScore, scoredValue.getValue)) + case _ => None + } 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))) + delegateRedisClusterCommandAndLift(_.zpopmax(key, count)).map(_.asScala.toList.collect { + case scoredValue if scoredValue.hasValue => 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 { @@ -44,6 +100,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 +118,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 +183,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 +204,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 +225,17 @@ private[this] object LettuceRedisSortedSetAsyncCommands{ } Range.create(toJavaNumber(range.start), toJavaNumber(range.end)) } + + + private[commands] def zAddOptionsToJava(options: Set[ZAddOptions]): ZAddArgs = { + val args = new ZAddArgs() + options.foreach { + case ZAddOptions.NX => args.nx() + case ZAddOptions.XX => args.xx() + case ZAddOptions.CH => args.ch() + case ZAddOptions.GT => args.gt() + case ZAddOptions.LT => args.lt() + } + 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..3003d0b 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 @@ -4,7 +4,7 @@ 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 com.github.scoquelin.arugula.commands.RedisSortedSetAsyncCommands.{RangeLimit, ScoreWithKeyValue, ScoreWithValue, ZAddOptions, ZRange} import org.scalatest.matchers.should.Matchers import scala.concurrent.duration._ @@ -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(ZAddOptions.NX), 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(ZAddOptions.NX), 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,71 @@ 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) + zRankWithScore <- client.zRankWithScore(key, "four") + _ <- zRankWithScore shouldBe Some(ScoreWithValue(2.0, 1)) + zRevRankResult <- client.zRevRank(key, "four") + _ <- zRevRankResult shouldBe Some(2L) + zRevRankWithScore <- client.zRevRankWithScore(key, "four") + _ <- zRevRankWithScore shouldBe Some(ScoreWithValue(2.0, 2)) + zAddIncr <- client.zAddIncr(key, ZAddOptions(ZAddOptions.XX), 2.9, "four") + _ <- zAddIncr shouldBe Some(4.9) + } yield succeed + } + } + + "support multi-key operations" in { + withRedisSingleNodeAndCluster(RedisCodec.Utf8WithValueAsStringCodec) { client => + val suffix = "{user1}" + val key1 = randomKey("sorted-set1") + suffix + val key2 = randomKey("sorted-set2") + suffix + val key3 = randomKey("sorted-set3") + suffix + for { + _ <- client.zAdd(key1, ScoreWithValue(1, "one"), ScoreWithValue(2, "two"), ScoreWithValue(3, "three")) + _ <- client.zAdd(key2, ScoreWithValue(4, "four"), ScoreWithValue(5, "five"), ScoreWithValue(6, "six")) + _ <- client.zAdd(key3, ScoreWithValue(7, "seven"), ScoreWithValue(8, "eight"), ScoreWithValue(9, "nine")) + bzPopMin <- client.bzPopMin(0.1, key1, key2, key3) + _ <- bzPopMin shouldBe Some(ScoreWithKeyValue(1, key1, "one")) + bzPopMax <- client.bzPopMax(0.1, key3, key2, key1) + _ <- bzPopMax shouldBe Some(ScoreWithKeyValue(9, key3, "nine")) + } 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..6e1d965 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,10 +1,9 @@ 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 io.lettuce.core.{RedisFuture, ScoredValue, ScoredValueScanCursor} +import com.github.scoquelin.arugula.commands.RedisSortedSetAsyncCommands +import com.github.scoquelin.arugula.commands.RedisSortedSetAsyncCommands.{RangeLimit, ScoreWithValue, ZAddOptions, ZRange} +import io.lettuce.core.{KeyValue, RedisFuture, ScoredValue, ScoredValueScanCursor} import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers.{any, eq => meq} import org.mockito.Mockito.{verify, when} @@ -19,6 +18,22 @@ class LettuceRedisSortedSetAsyncCommandsSpec extends wordspec.FixtureAsyncWordSp withFixture(test.toNoArgAsyncTest(new LettuceRedisCommandsClientFixture.TestContext)) "LettuceRedisSortedSetAsyncCommands" should { + + "delegate BZPOPMIN command to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue: KeyValue[String,ScoredValue[String]] = KeyValue.just("key", ScoredValue.just(1.0, "one")) + val mockRedisFuture: RedisFuture[KeyValue[String, ScoredValue[String]]] = mockRedisFutureToReturn(expectedValue) + + when(lettuceAsyncCommands.bzpopmin(0, "key")).thenReturn(mockRedisFuture) + + testClass.bzPopMin(0, "key").map { result => + result mustBe Some(RedisSortedSetAsyncCommands.ScoreWithKeyValue(1.0, "key", "one")) + verify(lettuceAsyncCommands).bzpopmin(0, "key") + succeed + } + } + "delegate ZADD command to Lettuce and lift result into a Future" in { testContext => import testContext._ @@ -27,13 +42,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, 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, 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 +187,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 +219,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 +313,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 +502,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 expectedValue = "one" + val mockRedisFuture: RedisFuture[String] = mockRedisFutureToReturn(expectedValue) + + when(lettuceAsyncCommands.zrandmember("key")).thenReturn(mockRedisFuture) + + testClass.zRandMember("key").map { result => + result mustBe Some(expectedValue) + verify(lettuceAsyncCommands).zrandmember("key") + succeed + } + } + + "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 mockHsetRedisFuture: RedisFuture[java.lang.Boolean] = mockRedisFutureToReturn(true) - when(lettuceAsyncCommands.hset("userKey", "sessionId", "token")).thenReturn(mockHsetRedisFuture) + val expectedValue = ScoredValue.just(1, "one") + val mockRedisFuture: RedisFuture[ScoredValue[String]] = mockRedisFutureToReturn(expectedValue) + + when(lettuceAsyncCommands.zrandmemberWithScores("key")).thenReturn(mockRedisFuture) - val mockExpireRedisFuture: RedisFuture[java.lang.Boolean] = mockRedisFutureToReturn(true) - when(lettuceAsyncCommands.expire("userKey", 24.hours.toSeconds)).thenReturn(mockExpireRedisFuture) + testClass.zRandMemberWithScores("key").map { result => + result mustBe Some(ScoreWithValue(1, "one")) + verify(lettuceAsyncCommands).zrandmemberWithScores("key") + succeed + } + } - val commands: RedisCommandsClient[String, String] => List[Future[Any]] = availableCommands => List( - availableCommands.hSet("userKey", "sessionId", "token"), - availableCommands.expire("userKey", 24.hours) - ) + "delegate ZRANDMEMBER WITHSCORES command with count to Lettuce and lift result into a Future" in { testContext => + import testContext._ - testClass.pipeline(commands).map { results => - results mustBe Some(List(true, true)) - verify(lettuceAsyncCommands).hset("userKey", "sessionId", "token") - verify(lettuceAsyncCommands).expire("userKey", 24.hours.toSeconds) + 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 } } } - }