Skip to content

Commit

Permalink
#12 implement string range and string multi related commands
Browse files Browse the repository at this point in the history
  • Loading branch information
John Loehrer committed Jul 29, 2024
1 parent 9b7c58f commit c417bb2
Show file tree
Hide file tree
Showing 4 changed files with 299 additions and 11 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.github.scoquelin.arugula.commands

import scala.collection.immutable.ListMap
import scala.concurrent.Future
import scala.concurrent.duration.FiniteDuration

Expand All @@ -10,6 +11,15 @@ import scala.concurrent.duration.FiniteDuration
* @tparam V The value type
*/
trait RedisStringAsyncCommands[K, V] {

/**
* Append a value to a key.
* @param key The key
* @param value The value
* @return The length of the string after the append operation
*/
def append(key: K, value: V): Future[Long]

/**
* Get the value of a key.
* @param key The key
Expand All @@ -24,6 +34,24 @@ trait RedisStringAsyncCommands[K, V] {
*/
def getDel(key: K): Future[Option[V]]


/**
* Get the value of a key and set its expiration.
* @param key The key
* @param expiresIn The expiration time
* @return
*/
def getEx(key: K, expiresIn: FiniteDuration): Future[Option[V]]

/**
* Get the value of a key and set a new value.
* @param key The key
* @param start The start index
* @param end The end index
* @return The value of the key
*/
def getRange(key: K, start: Long, end: Long): Future[Option[V]]

/**
* Get the value of a key and set a new value.
* @param key The key
Expand All @@ -32,6 +60,28 @@ trait RedisStringAsyncCommands[K, V] {
*/
def getSet(key: K, value: V): Future[Option[V]]

/**
* Get the values of all the given keys.
* @param keys The keys
* @return The values of the keys, in the same order as the keys
*/
def mGet(keys: K*): Future[ListMap[K, Option[V]]]

/**
* Set multiple keys to multiple values.
* @param keyValues The key-value pairs
* @return Unit
*/
def mSet(keyValues: Map[K, V]): Future[Unit]


/**
* Set multiple keys to multiple values, only if none of the keys exist.
* @param keyValues The key-value pairs
* @return true if all keys were set, false otherwise
*/
def mSetNx(keyValues: Map[K, V]): Future[Boolean]

/**
* Set the value of a key.
* @param key The key
Expand All @@ -56,6 +106,22 @@ trait RedisStringAsyncCommands[K, V] {
*/
def setNx(key: K, value: V): Future[Boolean]

/**
* Overwrite part of a string at key starting at the specified offset.
* @param key The key
* @param offset The offset
* @param value The value
* @return The length of the string after the append operation
*/
def setRange(key: K, offset: Long, value: V): Future[Long]

/**
* Get the length of the value stored in a key.
* @param key The key
* @return The length of the string at key, or 0 if the key does not exist
*/
def strLen(key: K): Future[Long]

/**
* Increment the integer value of a key by one.
* @param key The key
Expand Down Expand Up @@ -96,13 +162,6 @@ trait RedisStringAsyncCommands[K, V] {


/*** commands that are not yet implemented ***/
// def append(key: K, value: V): Future[Long]
// def getRange(key: K, start: Long, end: Long): Future[Option[V]]
// def setRange(key: K, offset: Long, value: V): Future[Long]
// def getEx(key: K, expiresIn: FiniteDuration): Future[Option[V]]
// def mGet(keys: K*): Future[Seq[Option[V]]]
// def mSet(keyValues: Map[K, V]): Future[Unit]
// def mSetNx(keyValues: Map[K, V]): Future[Boolean]
// def bitCount(key: K, start: Option[Long] = None, end: Option[Long] = None): Future[Long]
// def bitOpAnd(destination: K, keys: K*): Future[Long]
// def bitOpOr(destination: K, keys: K*): Future[Long]
Expand All @@ -111,7 +170,6 @@ trait RedisStringAsyncCommands[K, V] {
// def bitPos(key: K, bit: Boolean, start: Option[Long] = None, end: Option[Long] = None): Future[Long]
// def bitField(key: K, command: String, offset: Long, value: Option[Long] = None): Future[Long]
// def strAlgoLcs(keys: K*): Future[Option[V]]
// def strLen(key: K): Future[Long]
// def getBit(key: K, offset: Long): Future[Boolean]
// def setBit(key: K, offset: Long, value: Boolean): Future[Boolean]

Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,55 @@
package com.github.scoquelin.arugula.commands

import scala.collection.immutable.ListMap
import scala.concurrent.Future
import scala.concurrent.duration.FiniteDuration
import scala.jdk.CollectionConverters._

import com.github.scoquelin.arugula.internal.LettuceRedisCommandDelegation
import io.lettuce.core.{GetExArgs, KeyValue}

import java.util.concurrent.TimeUnit

private[arugula] trait LettuceRedisStringAsyncCommands[K, V] extends RedisStringAsyncCommands[K, V] with LettuceRedisCommandDelegation[K, V] {

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

override def get(key: K): Future[Option[V]] =
delegateRedisClusterCommandAndLift(_.get(key)).map(Option.apply)

override def getDel(key: K): Future[Option[V]] =
delegateRedisClusterCommandAndLift(_.getdel(key)).map(Option.apply)

override def getEx(key: K, expiresIn: FiniteDuration): Future[Option[V]] =
(expiresIn.unit match {
case TimeUnit.MILLISECONDS | TimeUnit.MICROSECONDS | TimeUnit.NANOSECONDS =>
delegateRedisClusterCommandAndLift(_.getex(key, GetExArgs.Builder.ex(expiresIn.toMillis)))
case _ =>
delegateRedisClusterCommandAndLift(_.getex(key, GetExArgs.Builder.ex(java.time.Duration.ofSeconds(expiresIn.toSeconds))))
}).map(Option.apply)

override def getRange(key: K, start: Long, end: Long): Future[Option[V]] =
delegateRedisClusterCommandAndLift(_.getrange(key, start, end)).map(Option.apply)

override def getSet(key: K, value: V): Future[Option[V]] =
delegateRedisClusterCommandAndLift(_.getset(key, value)).map(Option.apply)

override def mGet(keys: K*): Future[ListMap[K, Option[V]]] =
delegateRedisClusterCommandAndLift(_.mget(keys: _*)).map {
case null => ListMap.empty
case kvs => ListMap(kvs.toArray.map{
case kv: KeyValue[K, V] if kv.hasValue => kv.getKey -> Some(kv.getValue)
case kv: KeyValue[K, V] => kv.getKey -> None
}: _*)
}

override def mSet(keyValues: Map[K, V]): Future[Unit] =
delegateRedisClusterCommandAndLift(_.mset(keyValues.asJava)).map(_ => ())

override def mSetNx(keyValues: Map[K, V]): Future[Boolean] =
delegateRedisClusterCommandAndLift(_.msetnx(keyValues.asJava)).map(Boolean2boolean)

override def set(key: K, value: V): Future[Unit] =
delegateRedisClusterCommandAndLift(_.set(key, value)).map(_ => ())

Expand All @@ -30,6 +64,12 @@ private[arugula] trait LettuceRedisStringAsyncCommands[K, V] extends RedisString
override def setNx(key: K, value: V): Future[Boolean] =
delegateRedisClusterCommandAndLift(_.setnx(key, value)).map(Boolean2boolean)

override def setRange(key: K, offset: Long, value: V): Future[Long] =
delegateRedisClusterCommandAndLift(_.setrange(key, offset, value)).map(Long2long)

override def strLen(key: K): Future[Long] =
delegateRedisClusterCommandAndLift(_.strlen(key)).map(Long2long)

override def incr(key: K): Future[Long] =
delegateRedisClusterCommandAndLift(_.incr(key)).map(Long2long)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package com.github.scoquelin.arugula

import scala.collection.immutable.ListMap

import com.github.scoquelin.arugula.commands.RedisSortedSetAsyncCommands.{RangeLimit, ScoreWithValue, ZAddOptions, ZRange}
import org.scalatest.matchers.should.Matchers
import scala.concurrent.duration._

import java.util.concurrent.TimeUnit

class RedisCommandsIntegrationSpec extends BaseRedisCommandsIntegrationSpec with Matchers {
import RedisCommandsIntegrationSpec.randomKey

Expand Down Expand Up @@ -118,12 +122,23 @@ class RedisCommandsIntegrationSpec extends BaseRedisCommandsIntegrationSpec with
keyValue <- client.get(key)
_ <- keyValue match {
case Some(expectedValue) => expectedValue shouldBe value
case None => fail()
case None => fail("Expected value not found")
}
ttl <- client.ttl(key)
_ <- ttl match {
case Some(timeToLive) => assert(timeToLive > (expireIn - 1.minute) && timeToLive <= expireIn)
case None => fail()
case None => fail("Expected time to live not found")
}
longDuration = FiniteDuration(3, TimeUnit.DAYS)
getExp <- client.getEx(key, longDuration)
_ <- getExp match {
case Some(expectedValue) => expectedValue shouldBe value
case None => fail("Expected value not found")
}
getTtl <- client.ttl(key)
_ <- getTtl match {
case Some(timeToLive) => assert(timeToLive > (longDuration - 1.minute) && timeToLive <= longDuration)
case None => fail("Expected time to live not found")
}
deleted <- client.del(key)
_ <- deleted shouldBe 1L
Expand All @@ -133,6 +148,48 @@ class RedisCommandsIntegrationSpec extends BaseRedisCommandsIntegrationSpec with
}
}

"support string range operations" in {
withRedisSingleNodeAndCluster { client =>
val key = randomKey("range-key")
for {
lenResult <- client.append(key, "Hello")
_ = lenResult shouldBe 5L
lenResult <- client.append(key, ", World!")
_ = lenResult shouldBe 13L
range <- client.getRange(key, 0, 4)
_ = range shouldBe Some("Hello")
range <- client.getRange(key, -6, -1)
_ = range shouldBe Some("World!")
_ = client.setRange(key, 7, "Redis")
updatedValue <- client.get(key)
_ = updatedValue shouldBe Some("Hello, Redis!")
strLen <- client.strLen(key)
_ = strLen shouldBe 13L
} yield succeed

}
}

"support multiple key operations" in {
withRedisSingleNodeAndCluster { client =>
val suffix = "{user1}"
val key1 = randomKey("k1") + suffix
val key2 = randomKey("k2") + suffix
val key3 = randomKey("k3") + suffix
val key4 = randomKey("k4") + suffix
for {
_ <- client.mSet(Map(key1 -> "value1", key2 -> "value2", key3 -> "value3"))
values <- client.mGet(key1, key2, key3, key4)
_ <- values shouldBe ListMap(key1 -> Some("value1"), key2 -> Some("value2"), key3 -> Some("value3"), key4 -> None)
nxResult <- client.mSetNx(Map(key4 -> "value4"))
_ = nxResult shouldBe true
values <- client.mGet(key1, key2, key3, key4)
_ = values shouldBe ListMap(key1 -> Some("value1"), key2 -> Some("value2"), key3 -> Some("value3"), key4 -> Some("value4"))
} yield succeed

}
}

}

"leveraging RedisListAsyncCommands" should {
Expand Down
Loading

0 comments on commit c417bb2

Please sign in to comment.