Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

support all of the Redis List commands #23 #24

Merged
merged 1 commit into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

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