Skip to content

Commit f565661

Browse files
authored
Merge pull request #14 from scoquelin/jl-str-range-and-multi
#12 implement string range and string multi related commands
2 parents 9b7c58f + 668e869 commit f565661

File tree

4 files changed

+298
-11
lines changed

4 files changed

+298
-11
lines changed

modules/api/src/main/scala/com/github/scoquelin/arugula/commands/RedisStringAsyncCommands.scala

Lines changed: 66 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.github.scoquelin.arugula.commands
22

3+
import scala.collection.immutable.ListMap
34
import scala.concurrent.Future
45
import scala.concurrent.duration.FiniteDuration
56

@@ -10,6 +11,15 @@ import scala.concurrent.duration.FiniteDuration
1011
* @tparam V The value type
1112
*/
1213
trait RedisStringAsyncCommands[K, V] {
14+
15+
/**
16+
* Append a value to a key.
17+
* @param key The key
18+
* @param value The value
19+
* @return The length of the string after the append operation
20+
*/
21+
def append(key: K, value: V): Future[Long]
22+
1323
/**
1424
* Get the value of a key.
1525
* @param key The key
@@ -24,6 +34,24 @@ trait RedisStringAsyncCommands[K, V] {
2434
*/
2535
def getDel(key: K): Future[Option[V]]
2636

37+
38+
/**
39+
* Get the value of a key and set its expiration.
40+
* @param key The key
41+
* @param expiresIn The expiration time
42+
* @return
43+
*/
44+
def getEx(key: K, expiresIn: FiniteDuration): Future[Option[V]]
45+
46+
/**
47+
* Get the value of a key and set a new value.
48+
* @param key The key
49+
* @param start The start index
50+
* @param end The end index
51+
* @return The value of the key
52+
*/
53+
def getRange(key: K, start: Long, end: Long): Future[Option[V]]
54+
2755
/**
2856
* Get the value of a key and set a new value.
2957
* @param key The key
@@ -32,6 +60,28 @@ trait RedisStringAsyncCommands[K, V] {
3260
*/
3361
def getSet(key: K, value: V): Future[Option[V]]
3462

63+
/**
64+
* Get the values of all the given keys.
65+
* @param keys The keys
66+
* @return The values of the keys, in the same order as the keys
67+
*/
68+
def mGet(keys: K*): Future[ListMap[K, Option[V]]]
69+
70+
/**
71+
* Set multiple keys to multiple values.
72+
* @param keyValues The key-value pairs
73+
* @return Unit
74+
*/
75+
def mSet(keyValues: Map[K, V]): Future[Unit]
76+
77+
78+
/**
79+
* Set multiple keys to multiple values, only if none of the keys exist.
80+
* @param keyValues The key-value pairs
81+
* @return true if all keys were set, false otherwise
82+
*/
83+
def mSetNx(keyValues: Map[K, V]): Future[Boolean]
84+
3585
/**
3686
* Set the value of a key.
3787
* @param key The key
@@ -56,6 +106,22 @@ trait RedisStringAsyncCommands[K, V] {
56106
*/
57107
def setNx(key: K, value: V): Future[Boolean]
58108

109+
/**
110+
* Overwrite part of a string at key starting at the specified offset.
111+
* @param key The key
112+
* @param offset The offset
113+
* @param value The value
114+
* @return The length of the string after the append operation
115+
*/
116+
def setRange(key: K, offset: Long, value: V): Future[Long]
117+
118+
/**
119+
* Get the length of the value stored in a key.
120+
* @param key The key
121+
* @return The length of the string at key, or 0 if the key does not exist
122+
*/
123+
def strLen(key: K): Future[Long]
124+
59125
/**
60126
* Increment the integer value of a key by one.
61127
* @param key The key
@@ -96,13 +162,6 @@ trait RedisStringAsyncCommands[K, V] {
96162

97163

98164
/*** commands that are not yet implemented ***/
99-
// def append(key: K, value: V): Future[Long]
100-
// def getRange(key: K, start: Long, end: Long): Future[Option[V]]
101-
// def setRange(key: K, offset: Long, value: V): Future[Long]
102-
// def getEx(key: K, expiresIn: FiniteDuration): Future[Option[V]]
103-
// def mGet(keys: K*): Future[Seq[Option[V]]]
104-
// def mSet(keyValues: Map[K, V]): Future[Unit]
105-
// def mSetNx(keyValues: Map[K, V]): Future[Boolean]
106165
// def bitCount(key: K, start: Option[Long] = None, end: Option[Long] = None): Future[Long]
107166
// def bitOpAnd(destination: K, keys: K*): Future[Long]
108167
// def bitOpOr(destination: K, keys: K*): Future[Long]
@@ -111,7 +170,6 @@ trait RedisStringAsyncCommands[K, V] {
111170
// def bitPos(key: K, bit: Boolean, start: Option[Long] = None, end: Option[Long] = None): Future[Long]
112171
// def bitField(key: K, command: String, offset: Long, value: Option[Long] = None): Future[Long]
113172
// def strAlgoLcs(keys: K*): Future[Option[V]]
114-
// def strLen(key: K): Future[Long]
115173
// def getBit(key: K, offset: Long): Future[Boolean]
116174
// def setBit(key: K, offset: Long, value: Boolean): Future[Boolean]
117175

modules/core/src/main/scala/com/github/scoquelin/arugula/commands/LettuceRedisStringAsyncCommands.scala

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,55 @@
11
package com.github.scoquelin.arugula.commands
22

3+
import scala.collection.immutable.ListMap
34
import scala.concurrent.Future
45
import scala.concurrent.duration.FiniteDuration
6+
import scala.jdk.CollectionConverters._
7+
58
import com.github.scoquelin.arugula.internal.LettuceRedisCommandDelegation
9+
import io.lettuce.core.{GetExArgs, KeyValue}
610

711
import java.util.concurrent.TimeUnit
812

913
private[arugula] trait LettuceRedisStringAsyncCommands[K, V] extends RedisStringAsyncCommands[K, V] with LettuceRedisCommandDelegation[K, V] {
14+
15+
override def append(key: K, value: V): Future[Long] =
16+
delegateRedisClusterCommandAndLift(_.append(key, value)).map(Long2long)
17+
1018
override def get(key: K): Future[Option[V]] =
1119
delegateRedisClusterCommandAndLift(_.get(key)).map(Option.apply)
1220

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

24+
override def getEx(key: K, expiresIn: FiniteDuration): Future[Option[V]] =
25+
(expiresIn.unit match {
26+
case TimeUnit.MILLISECONDS | TimeUnit.MICROSECONDS | TimeUnit.NANOSECONDS =>
27+
delegateRedisClusterCommandAndLift(_.getex(key, GetExArgs.Builder.ex(expiresIn.toMillis)))
28+
case _ =>
29+
delegateRedisClusterCommandAndLift(_.getex(key, GetExArgs.Builder.ex(java.time.Duration.ofSeconds(expiresIn.toSeconds))))
30+
}).map(Option.apply)
31+
32+
override def getRange(key: K, start: Long, end: Long): Future[Option[V]] =
33+
delegateRedisClusterCommandAndLift(_.getrange(key, start, end)).map(Option.apply)
34+
1635
override def getSet(key: K, value: V): Future[Option[V]] =
1736
delegateRedisClusterCommandAndLift(_.getset(key, value)).map(Option.apply)
1837

38+
override def mGet(keys: K*): Future[ListMap[K, Option[V]]] =
39+
delegateRedisClusterCommandAndLift(_.mget(keys: _*)).map {
40+
case null => ListMap.empty
41+
case kvs => ListMap.from(kvs.asScala.collect {
42+
case keyValue =>
43+
if (keyValue.hasValue) keyValue.getKey -> Some(keyValue.getValue) else keyValue.getKey -> None
44+
})
45+
}
46+
47+
override def mSet(keyValues: Map[K, V]): Future[Unit] =
48+
delegateRedisClusterCommandAndLift(_.mset(keyValues.asJava)).map(_ => ())
49+
50+
override def mSetNx(keyValues: Map[K, V]): Future[Boolean] =
51+
delegateRedisClusterCommandAndLift(_.msetnx(keyValues.asJava)).map(Boolean2boolean)
52+
1953
override def set(key: K, value: V): Future[Unit] =
2054
delegateRedisClusterCommandAndLift(_.set(key, value)).map(_ => ())
2155

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

67+
override def setRange(key: K, offset: Long, value: V): Future[Long] =
68+
delegateRedisClusterCommandAndLift(_.setrange(key, offset, value)).map(Long2long)
69+
70+
override def strLen(key: K): Future[Long] =
71+
delegateRedisClusterCommandAndLift(_.strlen(key)).map(Long2long)
72+
3373
override def incr(key: K): Future[Long] =
3474
delegateRedisClusterCommandAndLift(_.incr(key)).map(Long2long)
3575

modules/tests/it/src/test/scala/com/github/scoquelin/arugula/RedisCommandsIntegrationSpec.scala

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
package com.github.scoquelin.arugula
22

3+
import scala.collection.immutable.ListMap
4+
35
import com.github.scoquelin.arugula.commands.RedisSortedSetAsyncCommands.{RangeLimit, ScoreWithValue, ZAddOptions, ZRange}
46
import org.scalatest.matchers.should.Matchers
57
import scala.concurrent.duration._
68

9+
import java.util.concurrent.TimeUnit
10+
711
class RedisCommandsIntegrationSpec extends BaseRedisCommandsIntegrationSpec with Matchers {
812
import RedisCommandsIntegrationSpec.randomKey
913

@@ -118,12 +122,23 @@ class RedisCommandsIntegrationSpec extends BaseRedisCommandsIntegrationSpec with
118122
keyValue <- client.get(key)
119123
_ <- keyValue match {
120124
case Some(expectedValue) => expectedValue shouldBe value
121-
case None => fail()
125+
case None => fail("Expected value not found")
122126
}
123127
ttl <- client.ttl(key)
124128
_ <- ttl match {
125129
case Some(timeToLive) => assert(timeToLive > (expireIn - 1.minute) && timeToLive <= expireIn)
126-
case None => fail()
130+
case None => fail("Expected time to live not found")
131+
}
132+
longDuration = FiniteDuration(3, TimeUnit.DAYS)
133+
getExp <- client.getEx(key, longDuration)
134+
_ <- getExp match {
135+
case Some(expectedValue) => expectedValue shouldBe value
136+
case None => fail("Expected value not found")
137+
}
138+
getTtl <- client.ttl(key)
139+
_ <- getTtl match {
140+
case Some(timeToLive) => assert(timeToLive > (longDuration - 1.minute) && timeToLive <= longDuration)
141+
case None => fail("Expected time to live not found")
127142
}
128143
deleted <- client.del(key)
129144
_ <- deleted shouldBe 1L
@@ -133,6 +148,48 @@ class RedisCommandsIntegrationSpec extends BaseRedisCommandsIntegrationSpec with
133148
}
134149
}
135150

151+
"support string range operations" in {
152+
withRedisSingleNodeAndCluster { client =>
153+
val key = randomKey("range-key")
154+
for {
155+
lenResult <- client.append(key, "Hello")
156+
_ = lenResult shouldBe 5L
157+
lenResult <- client.append(key, ", World!")
158+
_ = lenResult shouldBe 13L
159+
range <- client.getRange(key, 0, 4)
160+
_ = range shouldBe Some("Hello")
161+
range <- client.getRange(key, -6, -1)
162+
_ = range shouldBe Some("World!")
163+
_ = client.setRange(key, 7, "Redis")
164+
updatedValue <- client.get(key)
165+
_ = updatedValue shouldBe Some("Hello, Redis!")
166+
strLen <- client.strLen(key)
167+
_ = strLen shouldBe 13L
168+
} yield succeed
169+
170+
}
171+
}
172+
173+
"support multiple key operations" in {
174+
withRedisSingleNodeAndCluster { client =>
175+
val suffix = "{user1}"
176+
val key1 = randomKey("k1") + suffix
177+
val key2 = randomKey("k2") + suffix
178+
val key3 = randomKey("k3") + suffix
179+
val key4 = randomKey("k4") + suffix
180+
for {
181+
_ <- client.mSet(Map(key1 -> "value1", key2 -> "value2", key3 -> "value3"))
182+
values <- client.mGet(key1, key2, key3, key4)
183+
_ <- values shouldBe ListMap(key1 -> Some("value1"), key2 -> Some("value2"), key3 -> Some("value3"), key4 -> None)
184+
nxResult <- client.mSetNx(Map(key4 -> "value4"))
185+
_ = nxResult shouldBe true
186+
values <- client.mGet(key1, key2, key3, key4)
187+
_ = values shouldBe ListMap(key1 -> Some("value1"), key2 -> Some("value2"), key3 -> Some("value3"), key4 -> Some("value4"))
188+
} yield succeed
189+
190+
}
191+
}
192+
136193
}
137194

138195
"leveraging RedisListAsyncCommands" should {

0 commit comments

Comments
 (0)