Skip to content

Commit 00bf027

Browse files
committed
Add all Set commands with unit tests
This contributes to #10
1 parent 571d7bc commit 00bf027

File tree

5 files changed

+506
-1
lines changed

5 files changed

+506
-1
lines changed
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
import Foundation
2+
import NIO
3+
4+
extension RedisConnection {
5+
/// Returns the all of the elements of the set stored at key.
6+
///
7+
/// Ordering of results are stable between multiple calls of this method to the same set.
8+
///
9+
/// Results are **UNSTABLE** in regards to the ordering of insertions through the `sadd` command and this method.
10+
///
11+
/// [https://redis.io/commands/smembers](https://redis.io/commands/smembers)
12+
public func smembers(_ key: String) -> EventLoopFuture<RESPValue> {
13+
return command("SMEMBERS", arguments: [RESPValue(bulk: key)])
14+
}
15+
16+
/// Checks if the provided item is included in the set stored at key.
17+
///
18+
/// https://redis.io/commands/sismember
19+
/// - Parameter item: The element to look in the set for, stored as a `bulkString`.
20+
public func sismember(_ key: String, item: RESPValueConvertible) -> EventLoopFuture<Bool> {
21+
return send(command: "SISMEMBER", with: [key, item])
22+
.flatMapThrowing {
23+
guard let result = $0.int else { throw RedisError.respConversion(to: Int.self) }
24+
return result == 1
25+
}
26+
}
27+
28+
/// Returns the total count of elements in the set stored at key.
29+
///
30+
/// [https://redis.io/commands/scard](https://redis.io/commands/scard)
31+
public func scard(_ key: String) -> EventLoopFuture<Int> {
32+
return command("SCARD", arguments: [RESPValue(bulk: key)])
33+
.flatMapThrowing {
34+
guard let count = $0.int else { throw RedisError.respConversion(to: Int.self) }
35+
return count
36+
}
37+
}
38+
39+
/// Adds the provided items to the set stored at key, returning the count of items added.
40+
///
41+
/// [https://redis.io/commands/sadd](https://redis.io/commands/sadd)
42+
/// - Parameter items: The elements to add to the set, stored as `bulkString`s.
43+
public func sadd(_ key: String, items: [RESPValueConvertible]) -> EventLoopFuture<Int> {
44+
assert(items.count > 0, "There must be at least 1 item to add.")
45+
46+
return send(command: "SADD", with: [key] + items)
47+
.flatMapThrowing {
48+
guard let result = $0.int else { throw RedisError.respConversion(to: Int.self) }
49+
return result
50+
}
51+
}
52+
53+
/// Removes the provided items from the set stored at key, returning the count of items removed.
54+
///
55+
/// [https://redis.io/commands/srem](https://redis.io/commands/srem)
56+
/// - Parameter items: The elemnts to remove from the set, stored as `bulkString`s.
57+
public func srem(_ key: String, items: [RESPValueConvertible]) -> EventLoopFuture<Int> {
58+
assert(items.count > 0, "There must be at least 1 item listed to remove.")
59+
60+
return send(command: "SREM", with: [key] + items)
61+
.flatMapThrowing {
62+
guard let result = $0.int else { throw RedisError.respConversion(to: Int.self) }
63+
return result
64+
}
65+
}
66+
67+
/// Randomly selects an item from the set stored at key, and removes it.
68+
///
69+
/// [https://redis.io/commands/spop](https://redis.io/commands/spop)
70+
public func spop(_ key: String) -> EventLoopFuture<RESPValue> {
71+
return command("SPOP", arguments: [RESPValue(bulk: key)])
72+
}
73+
74+
/// Randomly selects elements from the set stored at string, up to the `count` provided.
75+
/// Use the `RESPValue.array` property to access the underlying values.
76+
///
77+
/// connection.srandmember("my_key") // pulls just one random element
78+
/// connection.srandmember("my_key", max: -3) // pulls up to 3 elements, allowing duplicates
79+
/// connection.srandmember("my_key", max: 3) // pulls up to 3 elements, guaranteed unique
80+
///
81+
/// [https://redis.io/commands/srandmember](https://redis.io/commands/srandmember)
82+
public func srandmember(_ key: String, max count: Int = 1) -> EventLoopFuture<RESPValue> {
83+
assert(count != 0, "A count of zero is a noop for selecting a random element.")
84+
85+
return command("SRANDMEMBER", arguments: [RESPValue(bulk: key), RESPValue(bulk: count.description)])
86+
}
87+
88+
/// Returns the members of the set resulting from the difference between the first set and all the successive sets.
89+
///
90+
/// [https://redis.io/commands/sdiff](https://redis.io/commands/sdiff)
91+
public func sdiff(_ keys: String...) -> EventLoopFuture<[RESPValue]> {
92+
return command("SDIFF", arguments: keys.map(RESPValue.init(bulk:)))
93+
.flatMapThrowing {
94+
guard let elements = $0.array else { throw RedisError.respConversion(to: Array<RESPValue>.self) }
95+
return elements
96+
}
97+
}
98+
99+
/// Functionally equivalent to `sdiff`, but instead stores the resulting set at the `destination` key
100+
/// and returns the count of elements in the result set.
101+
///
102+
/// [https://redis.io/commands/sdiffstore](https://redis.io/commands/sdiffstore)
103+
/// - Important: If the `destination` key already exists, it is overwritten.
104+
public func sdiffstore(destination dest: String, _ keys: String...) -> EventLoopFuture<Int> {
105+
return command("SDIFFSTORE", arguments: [RESPValue(bulk: dest)] + keys.map(RESPValue.init(bulk:)))
106+
.flatMapThrowing {
107+
guard let count = $0.int else { throw RedisError.respConversion(to: Int.self) }
108+
return count
109+
}
110+
}
111+
112+
/// Returns the members of the set resulting from the intersection of all the given sets.
113+
///
114+
/// [https://redis.io/commands/sinter](https://redis.io/commands/sinter)
115+
public func sinter(_ keys: String...) -> EventLoopFuture<[RESPValue]> {
116+
return command("SINTER", arguments: keys.map(RESPValue.init(bulk:)))
117+
.flatMapThrowing {
118+
guard let elements = $0.array else { throw RedisError.respConversion(to: Array<RESPValue>.self) }
119+
return elements
120+
}
121+
}
122+
123+
/// Functionally equivalent to `sinter`, but instead stores the resulting set at the `destination` key
124+
/// and returns the count of elements in the result set.
125+
///
126+
/// [https://redis.io/commands/sinterstore](https://redis.io/commands/sinterstore)
127+
/// - Important: If the `destination` key already exists, it is overwritten.
128+
public func sinterstore(destination dest: String, _ keys: String...) -> EventLoopFuture<Int> {
129+
return command("SINTERSTORE", arguments: [RESPValue(bulk: dest)] + keys.map(RESPValue.init(bulk:)))
130+
.flatMapThrowing {
131+
guard let count = $0.int else { throw RedisError.respConversion(to: Int.self) }
132+
return count
133+
}
134+
}
135+
136+
/// Moves the `item` from the source key to the destination key.
137+
///
138+
/// [https://redis.io/commands/smove](https://redis.io/commands/smove)
139+
/// - Important: This will resolve to `true` as long as it was successfully removed from the `source` key.
140+
public func smove(item: RESPValueConvertible, fromKey source: String, toKey dest: String) -> EventLoopFuture<Bool> {
141+
return send(command: "SMOVE", with: [source, dest, item])
142+
.flatMapThrowing {
143+
guard let result = $0.int else { throw RedisError.respConversion(to: Int.self) }
144+
return result == 1
145+
}
146+
}
147+
148+
/// Returns the members of the set resulting from the union of all the given keys.
149+
///
150+
/// [https://redis.io/commands/sunion](https://redis.io/commands/sunion)
151+
public func sunion(_ keys: String...) -> EventLoopFuture<[RESPValue]> {
152+
return command("SUNION", arguments: keys.map(RESPValue.init(bulk:)))
153+
.flatMapThrowing {
154+
guard let elements = $0.array else { throw RedisError.respConversion(to: Array<RESPValue>.self) }
155+
return elements
156+
}
157+
}
158+
159+
/// Functionally equivalent to `sunion`, but instead stores the resulting set at the `destination` key
160+
/// and returns the count of elements in the result set.
161+
///
162+
/// [https://redis.io/commands/sunionstore](https://redis.io/commands/sunionstore)
163+
/// - Important: If the `destination` key already exists, it is overwritten.
164+
public func sunionstore(destination dest: String, _ keys: String...) -> EventLoopFuture<Int> {
165+
return command("SUNIONSTORE", arguments: [RESPValue(bulk: dest)] + keys.map(RESPValue.init(bulk:)))
166+
.flatMapThrowing {
167+
guard let count = $0.int else { throw RedisError.respConversion(to: Int.self) }
168+
return count
169+
}
170+
}
171+
172+
/// Incrementally iterates over a set, returning a cursor position for additional calls with a limited collection
173+
/// of the entire set.
174+
///
175+
/// [https://redis.io/commands/sscan](https://redis.io/commands/sscan)
176+
/// - Parameters:
177+
/// - count: The number of elements to advance by. Redis default is 10.
178+
/// - matching: A glob-style pattern to filter values to be selected from the result set.
179+
public func sscan(
180+
_ key: String,
181+
atPosition pos: Int = 0,
182+
count: Int? = nil,
183+
matching match: String? = nil
184+
) -> EventLoopFuture<(Int, [RESPValue])> {
185+
var args: [RESPValueConvertible] = [key, pos]
186+
187+
if let m = match {
188+
args.append("match")
189+
args.append(m)
190+
}
191+
if let c = count {
192+
args.append("count")
193+
args.append(c)
194+
}
195+
196+
return send(command: "SSCAN", with: args)
197+
.flatMapThrowing {
198+
guard let response = $0.array else { throw RedisError.respConversion(to: Array<RESPValue>.self) }
199+
guard
200+
let position = response[0].string,
201+
let newPosition = Int(position)
202+
else { throw RedisError.respConversion(to: Int.self) }
203+
guard let elements = response[1].array else { throw RedisError.respConversion(to: Array<RESPValue>.self) }
204+
205+
return (newPosition, elements)
206+
}
207+
}
208+
}

Sources/NIORedis/RedisConnection.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public final class RedisConnection {
4242
/// - command: The command to execute.
4343
/// - arguments: The arguments to be sent with the command.
4444
/// - Returns: An `EventLoopFuture` that will resolve with the Redis command response.
45-
public func send(command: String, with arguments: [RESPValueConvertible] = []) throws -> EventLoopFuture<RESPValue> {
45+
public func send(command: String, with arguments: [RESPValueConvertible] = []) -> EventLoopFuture<RESPValue> {
4646
let args = arguments.map { $0.convertedToRESPValue() }
4747
return self.command(command, arguments: args)
4848
}

Sources/NIORedis/RedisError.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,8 @@ extension RedisError {
2323
internal static var connectionClosed: RedisError {
2424
return RedisError(identifier: "connection", reason: "Connection was closed while trying to execute.")
2525
}
26+
27+
internal static func respConversion<T>(to dest: T.Type) -> RedisError {
28+
return RedisError(identifier: "respConversion", reason: "Failed to convert RESP to \(String(describing: dest))")
29+
}
2630
}

0 commit comments

Comments
 (0)