Skip to content

Commit

Permalink
support all of the Redis List commands #23
Browse files Browse the repository at this point in the history
  • Loading branch information
John Loehrer committed Aug 8, 2024
1 parent c6b6572 commit a73769d
Show file tree
Hide file tree
Showing 4 changed files with 238 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,49 @@ package com.github.scoquelin.arugula.commands
import scala.concurrent.Future

trait RedisListAsyncCommands[K, V] {
def blMove(
source: K,
destination: K,
sourceSide: RedisListAsyncCommands.Side = RedisListAsyncCommands.Side.Right,
destinationSide: RedisListAsyncCommands.Side = RedisListAsyncCommands.Side.Left,
timeout: Double = 0.0, // zero is infinite wait
): Future[Option[V]]
def blMPop(
keys: List[K],
direction:RedisListAsyncCommands.Side = RedisListAsyncCommands.Side.Left,
count: Int = 1,
timeout: Double = 0.0,
): Future[Option[(K, List[V])]]
def lMPop(
keys: List[K],
direction:RedisListAsyncCommands.Side = RedisListAsyncCommands.Side.Left,
count: Int = 1,
): Future[Option[(K, List[V])]]
def lPush(key: K, values: V*): Future[Long]
def rPush(key: K, values: V*): Future[Long]
def lPop(key: K): Future[Option[V]]
def rPop(key: K): Future[Option[V]]
def lRange(key: K, start: Long, end: Long): Future[List[V]]
def lMove(
source: K,
destination: K,
sourceSide: RedisListAsyncCommands.Side,
destinationSide: RedisListAsyncCommands.Side
): Future[Option[V]]
def lPos(key: K, value: V): Future[Option[Long]]
def lLen(key: K): Future[Long]
def lRem(key: K, count: Long, value: V): Future[Long]
def lTrim(key: K, start: Long, end: Long): Future[Unit]
def lIndex(key: K, index: Long): Future[Option[V]]
}

object RedisListAsyncCommands {
sealed trait Side

object Side {
case object Left extends Side

case object Right extends Side

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,63 @@ import scala.concurrent.Future
import scala.jdk.CollectionConverters._

import com.github.scoquelin.arugula.internal.LettuceRedisCommandDelegation
import io.lettuce.core.{LMPopArgs, LMoveArgs}

private[arugula] trait LettuceRedisListAsyncCommands[K, V] extends RedisListAsyncCommands[K, V] with LettuceRedisCommandDelegation[K, V]{

override def blMove(
source: K,
destination: K,
sourceSide: RedisListAsyncCommands.Side = RedisListAsyncCommands.Side.Right,
destinationSide: RedisListAsyncCommands.Side = RedisListAsyncCommands.Side.Left,
timeout: Double = 0.0, // zero is infinite wait
): Future[Option[V]] = {
val args = (sourceSide, destinationSide) match {
case (RedisListAsyncCommands.Side.Left, RedisListAsyncCommands.Side.Left) => LMoveArgs.Builder.leftLeft()
case (RedisListAsyncCommands.Side.Left, RedisListAsyncCommands.Side.Right) => LMoveArgs.Builder.leftRight()
case (RedisListAsyncCommands.Side.Right, RedisListAsyncCommands.Side.Left) => LMoveArgs.Builder.rightLeft()
case (RedisListAsyncCommands.Side.Right, RedisListAsyncCommands.Side.Right) => LMoveArgs.Builder.rightRight()
}
delegateRedisClusterCommandAndLift(_.blmove(source, destination, args, timeout)).map(Option.apply)
}

override def blMPop(
keys: List[K],
direction:RedisListAsyncCommands.Side = RedisListAsyncCommands.Side.Left,
count: Int = 1,
timeout: Double = 0.0,
): Future[Option[(K, List[V])]] = {
val args: LMPopArgs = direction match {
case RedisListAsyncCommands.Side.Left => LMPopArgs.Builder.left().count(count)
case RedisListAsyncCommands.Side.Right => LMPopArgs.Builder.right().count(count)
}
delegateRedisClusterCommandAndLift(_.blmpop(timeout, args, keys: _*)).map{
case null => None
case result =>
val key = result.getKey
val values = if(result.hasValue) result.getValue.asScala.toList else List.empty
Some((key, values))
}
}

def lMPop(
keys: List[K],
direction:RedisListAsyncCommands.Side = RedisListAsyncCommands.Side.Left,
count: Int = 1,
): Future[Option[(K, List[V])]] = {
val args: LMPopArgs = direction match {
case RedisListAsyncCommands.Side.Left => LMPopArgs.Builder.left().count(count)
case RedisListAsyncCommands.Side.Right => LMPopArgs.Builder.right().count(count)
}
delegateRedisClusterCommandAndLift(_.lmpop(args, keys: _*)).map{
case null => None
case result =>
val key = result.getKey
val values = if(result.hasValue) result.getValue.asScala.toList else List.empty
Some((key, values))
}
}

override def lRem(key: K, count: Long, value: V): Future[Long] =
delegateRedisClusterCommandAndLift(_.lrem(key, count, value)).map(Long2long)

Expand All @@ -16,6 +70,21 @@ private[arugula] trait LettuceRedisListAsyncCommands[K, V] extends RedisListAsyn
override def lRange(key: K, start: Long, stop: Long): Future[List[V]] =
delegateRedisClusterCommandAndLift(_.lrange(key, start, stop)).map(_.asScala.toList)

def lMove(
source: K,
destination: K,
sourceSide: RedisListAsyncCommands.Side,
destinationSide: RedisListAsyncCommands.Side
): Future[Option[V]] = {
val args = (sourceSide, destinationSide) match {
case (RedisListAsyncCommands.Side.Left, RedisListAsyncCommands.Side.Left) => LMoveArgs.Builder.leftLeft()
case (RedisListAsyncCommands.Side.Left, RedisListAsyncCommands.Side.Right) => LMoveArgs.Builder.leftRight()
case (RedisListAsyncCommands.Side.Right, RedisListAsyncCommands.Side.Left) => LMoveArgs.Builder.rightLeft()
case (RedisListAsyncCommands.Side.Right, RedisListAsyncCommands.Side.Right) => LMoveArgs.Builder.rightRight()
}
delegateRedisClusterCommandAndLift(_.lmove(source, destination, args)).map(Option.apply)
}

override def lPos(key: K, value: V): Future[Option[Long]] =
delegateRedisClusterCommandAndLift(_.lpos(key, value)).map(Option(_).map(Long2long))

Expand All @@ -38,3 +107,35 @@ private[arugula] trait LettuceRedisListAsyncCommands[K, V] extends RedisListAsyn
delegateRedisClusterCommandAndLift(_.lindex(key, index)).map(Option.apply)

}

object LettuceRedisListAsyncCommands {
sealed trait Side

object Side {
case object Left extends Side

case object Right extends Side
}

}

/**
* blpop
* brpop
* brpoplpush
* lindex
* linsert
* llen
* lpop
* lpos
* lpush
* lpushx
* lrange
* lrem
* lset
* ltrim
* rpop
* rpoplpush
* rpush
* rpushx
*/
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import scala.collection.immutable.ListMap
import com.github.scoquelin.arugula.codec.RedisCodec
import com.github.scoquelin.arugula.commands.RedisSortedSetAsyncCommands.{RangeLimit, ScoreWithValue, ZAddOptions, ZRange}
import org.scalatest.matchers.should.Matchers

import scala.concurrent.duration._

import com.github.scoquelin.arugula.commands.RedisListAsyncCommands
import com.github.scoquelin.arugula.commands.RedisStringAsyncCommands.{BitFieldCommand, BitFieldDataType}

import java.util.concurrent.TimeUnit
Expand Down Expand Up @@ -361,6 +361,52 @@ class RedisCommandsIntegrationSpec extends BaseRedisCommandsIntegrationSpec with
}
}

"support move operations" in {
withRedisSingleNodeAndCluster(RedisCodec.Utf8WithValueAsStringCodec) { client =>
val suffix = "{user1}"
val key1 = randomKey("list-key1") + suffix
val key2 = randomKey("list-key2") + suffix
val key3 = randomKey("list-key3") + suffix

for {
_ <- client.lPush(key1, "one", "two", "three")
_ <- client.lPush(key2, "four", "five", "six")
_ <- client.lMove(key1, key2, RedisListAsyncCommands.Side.Left, RedisListAsyncCommands.Side.Right)
_ <- client.blMove(key1, key3, RedisListAsyncCommands.Side.Left, RedisListAsyncCommands.Side.Right, timeout = 0.1)
key1Range <- client.lRange(key1, 0, -1)
_ <- key1Range shouldBe List("one")
key2Range <- client.lRange(key2, 0, -1)
_ <- key2Range shouldBe List("six", "five", "four", "three")
key3Range <- client.lRange(key3, 0, -1)
_ <- key3Range shouldBe List("two")

} yield succeed
}
}

"support multi pop operations" in {
withRedisSingleNodeAndCluster(RedisCodec.Utf8WithValueAsStringCodec) { client =>
val suffix = "{user1}"
val key1 = randomKey("list-key1") + suffix
val key2 = randomKey("list-key2") + suffix
for {
_ <- client.lPush(key1, "one", "two", "three")
_ <- client.lPush(key2, "four", "five", "six")
mPopResult <- client.lMPop(List(key1, key2), count = 2)
_ <- mPopResult shouldBe Some((key1, List("three", "two")))
key1Range <- client.lRange(key1, 0, -1)
_ <- key1Range shouldBe List("one")
key2Range <- client.lRange(key2, 0, -1)
_ <- key2Range shouldBe List("six", "five", "four")
blPopResult <- client.blMPop(List(key1, key2), count = 2, timeout = 0.1)
_ <- blPopResult shouldBe Some((key1, List("one")))
key1Range <- client.lRange(key1, 0, -1)
_ <- key1Range shouldBe List()
key2Range <- client.lRange(key2, 0, -1)
_ <- key2Range shouldBe List("six", "five", "four")
} yield succeed
}
}
}

"leveraging RedisSortedSetAsyncCommands" should {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ import scala.jdk.CollectionConverters._

import com.github.scoquelin.arugula.commands.RedisKeyAsyncCommands.ScanCursor
import com.github.scoquelin.arugula.commands.RedisSortedSetAsyncCommands.{RangeLimit, ScoreWithValue, ZRange}
import com.github.scoquelin.arugula.commands.RedisStringAsyncCommands.{BitFieldCommand, BitFieldDataType, BitFieldOperation}
import com.github.scoquelin.arugula.commands.RedisStringAsyncCommands.{BitFieldCommand, BitFieldDataType}
import com.github.scoquelin.arugula.connection.RedisConnection
import io.lettuce.core.{GetExArgs, KeyValue, MapScanCursor, RedisFuture, ScoredValue, ScoredValueScanCursor}
import com.github.scoquelin.arugula.commands.RedisListAsyncCommands
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers.{any, anyBoolean, anyLong, anyString, eq => meq}
import org.mockito.ArgumentMatchers.{any, anyBoolean, anyDouble, anyLong, anyString, eq => meq}
import org.mockito.Mockito.{verify, when}
import org.scalatest.matchers.must.Matchers
import org.scalatest.{FutureOutcome, wordspec}
Expand Down Expand Up @@ -888,6 +889,44 @@ class LettuceRedisCommandsClientSpec extends wordspec.FixtureAsyncWordSpec with
}
}

"delegate BLMOVE command to Lettuce and lift result into a Future" in { testContext =>
import testContext._

val expectedValue = "value"
val mockRedisFuture: RedisFuture[String] = mockRedisFutureToReturn(expectedValue)
when(lettuceAsyncCommands.blmove(any, any, any, anyDouble)).thenReturn(mockRedisFuture)

testClass.blMove("source", "destination", RedisListAsyncCommands.Side.Left, RedisListAsyncCommands.Side.Right, 1.0).map { result =>
result mustBe Some(expectedValue)
verify(lettuceAsyncCommands).blmove(meq("source"), meq("destination"), any, meq(1.0))
succeed
}
}

"delegate BLMPOP command to Lettuce and lift result into a Future" in { testContext =>
import testContext._
val expectedValue = KeyValue.fromNullable("key", List("value").asJava)
val mockRedisFuture: RedisFuture[KeyValue[String, java.util.List[String]]] = mockRedisFutureToReturn(expectedValue)
when(lettuceAsyncCommands.blmpop(anyDouble, any, anyString)).thenReturn(mockRedisFuture)
testClass.blMPop(List("key"), timeout=1).map { result =>
verify(lettuceAsyncCommands).blmpop(meq(1.0), any, meq("key"))
result mustBe Some(("key", List("value")))
succeed
}
}

"delegate LMPOP command to Lettuce and lift result into a Future" in { testContext =>
import testContext._
val expectedValue = KeyValue.fromNullable("key", List("value").asJava)
val mockRedisFuture: RedisFuture[KeyValue[String, java.util.List[String]]] = mockRedisFutureToReturn(expectedValue)
when(lettuceAsyncCommands.lmpop(any, anyString)).thenReturn(mockRedisFuture)
testClass.lMPop(List("key")).map { result =>
verify(lettuceAsyncCommands).lmpop(any, meq("key"))
result mustBe Some(("key", List("value")))
succeed
}
}

"delegate LPUSH command to Lettuce and lift result into a Future" in { testContext =>
import testContext._

Expand Down Expand Up @@ -963,6 +1002,21 @@ class LettuceRedisCommandsClientSpec extends wordspec.FixtureAsyncWordSpec with
}
}

"delegate LMOVE command to Lettuce and lift result into a Future" in { testContext =>
import testContext._

val expectedValue = "value"
val mockRedisFuture: RedisFuture[String] = mockRedisFutureToReturn(expectedValue)

when(lettuceAsyncCommands.lmove(any, any, any)).thenReturn(mockRedisFuture)

testClass.lMove("source", "destination", RedisListAsyncCommands.Side.Left, RedisListAsyncCommands.Side.Right).map { result =>
result mustBe Some(expectedValue)
verify(lettuceAsyncCommands).lmove(meq("source"), meq("destination"), any)
succeed
}
}

"delegate LPOS command to Lettuce and lift result into a Future" in { testContext =>
import testContext._

Expand Down

0 comments on commit a73769d

Please sign in to comment.