Skip to content

Commit 6b30127

Browse files
committed
Refactor RedisConnection and RedisPipeline into protocols.
Motivation: The goal of this commit is to make it easier for library users to implement their own types for creating connections and pipelines without losing all of the convenience command extensions. This also splits executing commands from the concept of a "connection" to make it more swifty in `RedisPipeline`.
1 parent 6825451 commit 6b30127

File tree

9 files changed

+170
-104
lines changed

9 files changed

+170
-104
lines changed

Sources/NIORedis/Commands/BasicCommands.swift

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11
import Foundation
22
import NIO
33

4-
extension RedisConnection {
4+
extension RedisCommandExecutor {
55
/// Select the Redis logical database having the specified zero-based numeric index.
6-
/// New connections always use the database 0.
6+
/// New connections always use the database `0`.
77
///
88
/// [https://redis.io/commands/select](https://redis.io/commands/select)
9-
public func select(_ id: Int) -> EventLoopFuture<Void> {
10-
return command("SELECT", arguments: [RESPValue(bulk: id.description)])
9+
public func select(database id: Int) -> EventLoopFuture<Void> {
10+
return send(command: "SELECT", with: [id.description])
1111
.map { _ in return () }
1212
}
1313

1414
/// Request for authentication in a password-protected Redis server.
1515
///
1616
/// [https://redis.io/commands/auth](https://redis.io/commands/auth)
1717
public func authorize(with password: String) -> EventLoopFuture<Void> {
18-
return command("AUTH", arguments: [RESPValue(bulk: password)])
18+
return send(command: "AUTH", with: [password])
1919
.map { _ in return () }
2020
}
2121

@@ -24,8 +24,7 @@ extension RedisConnection {
2424
/// [https://redis.io/commands/del](https://redis.io/commands/del)
2525
/// - Returns: A future number of keys that were removed.
2626
public func delete(_ keys: String...) -> EventLoopFuture<Int> {
27-
let keyArgs = keys.map { RESPValue(bulk: $0) }
28-
return command("DEL", arguments: keyArgs)
27+
return send(command: "DEL", with: keys)
2928
.flatMapThrowing { res in
3029
guard let count = res.int else {
3130
throw RedisError(identifier: "delete", reason: "Unexpected response: \(res)")
@@ -42,7 +41,7 @@ extension RedisConnection {
4241
/// - after: The lifetime (in seconds) the key will expirate at.
4342
/// - Returns: A future bool indicating if the expiration was set or not.
4443
public func expire(_ key: String, after deadline: Int) -> EventLoopFuture<Bool> {
45-
return command("EXPIRE", arguments: [RESPValue(bulk: key), RESPValue(bulk: deadline.description)])
44+
return send(command: "EXPIRE", with: [key, deadline.description])
4645
.flatMapThrowing { res in
4746
guard let value = res.int else {
4847
throw RedisError(identifier: "expire", reason: "Unexpected response: \(res)")
@@ -57,7 +56,7 @@ extension RedisConnection {
5756
///
5857
/// [https://redis.io/commands/get](https://redis.io/commands/get)
5958
public func get(_ key: String) -> EventLoopFuture<String?> {
60-
return command("GET", arguments: [RESPValue(bulk: key)])
59+
return send(command: "GET", with: [key])
6160
.map { return $0.string }
6261
}
6362

@@ -67,7 +66,7 @@ extension RedisConnection {
6766
///
6867
/// [https://redis.io/commands/set](https://redis.io/commands/set)
6968
public func set(_ key: String, to value: String) -> EventLoopFuture<Void> {
70-
return command("SET", arguments: [RESPValue(bulk: key), RESPValue(bulk: value)])
69+
return send(command: "SET", with: [key, value])
7170
.map { _ in return () }
7271
}
7372

Sources/NIORedis/Commands/SetCommands.swift

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Foundation
22
import NIO
33

4-
extension RedisConnection {
4+
extension RedisCommandExecutor {
55
/// Returns the all of the elements of the set stored at key.
66
///
77
/// Ordering of results are stable between multiple calls of this method to the same set.
@@ -10,7 +10,7 @@ extension RedisConnection {
1010
///
1111
/// [https://redis.io/commands/smembers](https://redis.io/commands/smembers)
1212
public func smembers(_ key: String) -> EventLoopFuture<RESPValue> {
13-
return command("SMEMBERS", arguments: [RESPValue(bulk: key)])
13+
return send(command: "SMEMBERS", with: [key])
1414
}
1515

1616
/// Checks if the provided item is included in the set stored at key.
@@ -29,7 +29,7 @@ extension RedisConnection {
2929
///
3030
/// [https://redis.io/commands/scard](https://redis.io/commands/scard)
3131
public func scard(_ key: String) -> EventLoopFuture<Int> {
32-
return command("SCARD", arguments: [RESPValue(bulk: key)])
32+
return send(command: "SCARD", with: [key])
3333
.flatMapThrowing {
3434
guard let count = $0.int else { throw RedisError.respConversion(to: Int.self) }
3535
return count
@@ -68,7 +68,7 @@ extension RedisConnection {
6868
///
6969
/// [https://redis.io/commands/spop](https://redis.io/commands/spop)
7070
public func spop(_ key: String) -> EventLoopFuture<RESPValue> {
71-
return command("SPOP", arguments: [RESPValue(bulk: key)])
71+
return send(command: "SPOP", with: [key])
7272
}
7373

7474
/// Randomly selects elements from the set stored at string, up to the `count` provided.
@@ -82,14 +82,14 @@ extension RedisConnection {
8282
public func srandmember(_ key: String, max count: Int = 1) -> EventLoopFuture<RESPValue> {
8383
assert(count != 0, "A count of zero is a noop for selecting a random element.")
8484

85-
return command("SRANDMEMBER", arguments: [RESPValue(bulk: key), RESPValue(bulk: count.description)])
85+
return send(command: "SRANDMEMBER", with: [key, count.description])
8686
}
8787

8888
/// Returns the members of the set resulting from the difference between the first set and all the successive sets.
8989
///
9090
/// [https://redis.io/commands/sdiff](https://redis.io/commands/sdiff)
9191
public func sdiff(_ keys: String...) -> EventLoopFuture<[RESPValue]> {
92-
return command("SDIFF", arguments: keys.map(RESPValue.init(bulk:)))
92+
return send(command: "SDIFF", with: keys)
9393
.flatMapThrowing {
9494
guard let elements = $0.array else { throw RedisError.respConversion(to: Array<RESPValue>.self) }
9595
return elements
@@ -102,7 +102,7 @@ extension RedisConnection {
102102
/// [https://redis.io/commands/sdiffstore](https://redis.io/commands/sdiffstore)
103103
/// - Important: If the `destination` key already exists, it is overwritten.
104104
public func sdiffstore(destination dest: String, _ keys: String...) -> EventLoopFuture<Int> {
105-
return command("SDIFFSTORE", arguments: [RESPValue(bulk: dest)] + keys.map(RESPValue.init(bulk:)))
105+
return send(command: "SDIFFSTORE", with: [dest] + keys)
106106
.flatMapThrowing {
107107
guard let count = $0.int else { throw RedisError.respConversion(to: Int.self) }
108108
return count
@@ -113,7 +113,7 @@ extension RedisConnection {
113113
///
114114
/// [https://redis.io/commands/sinter](https://redis.io/commands/sinter)
115115
public func sinter(_ keys: String...) -> EventLoopFuture<[RESPValue]> {
116-
return command("SINTER", arguments: keys.map(RESPValue.init(bulk:)))
116+
return send(command: "SINTER", with: keys)
117117
.flatMapThrowing {
118118
guard let elements = $0.array else { throw RedisError.respConversion(to: Array<RESPValue>.self) }
119119
return elements
@@ -126,7 +126,7 @@ extension RedisConnection {
126126
/// [https://redis.io/commands/sinterstore](https://redis.io/commands/sinterstore)
127127
/// - Important: If the `destination` key already exists, it is overwritten.
128128
public func sinterstore(destination dest: String, _ keys: String...) -> EventLoopFuture<Int> {
129-
return command("SINTERSTORE", arguments: [RESPValue(bulk: dest)] + keys.map(RESPValue.init(bulk:)))
129+
return send(command: "SINTERSTORE", with: [dest] + keys)
130130
.flatMapThrowing {
131131
guard let count = $0.int else { throw RedisError.respConversion(to: Int.self) }
132132
return count
@@ -149,7 +149,7 @@ extension RedisConnection {
149149
///
150150
/// [https://redis.io/commands/sunion](https://redis.io/commands/sunion)
151151
public func sunion(_ keys: String...) -> EventLoopFuture<[RESPValue]> {
152-
return command("SUNION", arguments: keys.map(RESPValue.init(bulk:)))
152+
return send(command: "SUNION", with: keys)
153153
.flatMapThrowing {
154154
guard let elements = $0.array else { throw RedisError.respConversion(to: Array<RESPValue>.self) }
155155
return elements
@@ -162,7 +162,7 @@ extension RedisConnection {
162162
/// [https://redis.io/commands/sunionstore](https://redis.io/commands/sunionstore)
163163
/// - Important: If the `destination` key already exists, it is overwritten.
164164
public func sunionstore(destination dest: String, _ keys: String...) -> EventLoopFuture<Int> {
165-
return command("SUNIONSTORE", arguments: [RESPValue(bulk: dest)] + keys.map(RESPValue.init(bulk:)))
165+
return send(command: "SUNIONSTORE", with: [dest] + keys)
166166
.flatMapThrowing {
167167
guard let count = $0.int else { throw RedisError.respConversion(to: Int.self) }
168168
return count
Lines changed: 59 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,62 @@
11
import NIO
22
import NIOConcurrencyHelpers
33

4-
/// A connection to a Redis database instance, with the ability to send and receive commands.
4+
/// An object capable of sending commands and receiving responses.
55
///
6-
/// let result = connection.send(command: "GET", arguments: ["my_key"]
6+
/// let executor = ...
7+
/// let result = executor.send(command: "GET", arguments: ["my_key"]
78
/// // result == EventLoopFuture<RESPValue>
89
///
910
/// See [https://redis.io/commands](https://redis.io/commands)
10-
public final class RedisConnection {
11+
public protocol RedisCommandExecutor {
12+
/// The `EventLoop` that this executor operates on.
13+
var eventLoop: EventLoop { get }
14+
15+
/// Sends the desired command with the specified arguments.
16+
/// - Parameters:
17+
/// - command: The command to execute.
18+
/// - arguments: The arguments, if any, to be sent with the command.
19+
/// - Returns: An `EventLoopFuture` that will resolve with the Redis command response.
20+
func send(command: String, with arguments: [RESPValueConvertible]) -> EventLoopFuture<RESPValue>
21+
}
22+
23+
extension RedisCommandExecutor {
24+
/// Sends the desired command without arguments.
25+
/// - Parameter command: The command keyword to execute.
26+
/// - Returns: An `EventLoopFuture` that will resolve with the Redis command response.
27+
func send(command: String) -> EventLoopFuture<RESPValue> {
28+
return self.send(command: command, with: [])
29+
}
30+
}
31+
32+
/// An individual connection to a Redis database instance for executing commands or building `RedisPipeline`s.
33+
///
34+
/// See `RedisCommandExecutor`.
35+
public protocol RedisConnection: AnyObject, RedisCommandExecutor {
1136
/// The `Channel` this connection is associated with.
37+
var channel: Channel { get }
38+
/// Has the connection been closed?
39+
var isClosed: Bool { get }
40+
41+
/// Creates a `RedisPipeline` for executing a batch of commands.
42+
func makePipeline() -> RedisPipeline
43+
44+
/// Closes the connection to Redis.
45+
/// - Returns: An `EventLoopFuture` that resolves when the connection has been closed.
46+
@discardableResult
47+
func close() -> EventLoopFuture<Void>
48+
}
49+
50+
extension RedisConnection {
51+
public var eventLoop: EventLoop { return self.channel.eventLoop }
52+
}
53+
54+
/// A basic `RedisConnection`.
55+
public final class NIORedisConnection: RedisConnection {
56+
/// See `RedisConnection.channel`.
1257
public let channel: Channel
1358

14-
/// Has the connection been closed?
59+
/// See `RedisConnection.isClosed`.
1560
public var isClosed: Bool { return _isClosed.load() }
1661
private var _isClosed = Atomic<Bool>(value: false)
1762

@@ -24,8 +69,7 @@ public final class RedisConnection {
2469
self.channel = channel
2570
}
2671

27-
/// Closes the connection to Redis.
28-
/// - Returns: An `EventLoopFuture` that resolves when the connection has been closed.
72+
/// See `RedisConnection.close()`.
2973
@discardableResult
3074
public func close() -> EventLoopFuture<Void> {
3175
guard !_isClosed.exchange(with: true) else { return channel.eventLoop.makeSucceededFuture(()) }
@@ -38,40 +82,25 @@ public final class RedisConnection {
3882
}
3983
}
4084

41-
/// Sends the desired command with the specified arguments.
42-
/// - Parameters:
43-
/// - command: The command to execute.
44-
/// - arguments: The arguments to be sent with the command.
45-
/// - Returns: An `EventLoopFuture` that will resolve with the Redis command response.
46-
public func send(command: String, with arguments: [RESPValueConvertible] = []) -> EventLoopFuture<RESPValue> {
47-
let args = arguments.map { $0.convertedToRESPValue() }
48-
return self.command(command, arguments: args)
85+
/// See `RedisConnection.makePipeline()`.
86+
public func makePipeline() -> RedisPipeline {
87+
return NIORedisPipeline(channel: channel)
4988
}
5089

51-
/// Invokes a command against Redis with the provided arguments.
52-
/// - Important: Arguments should be stored as `.bulkString`.
53-
/// - Parameters:
54-
/// - command: The command to execute.
55-
/// - arguments: The arguments to be sent with the command.
56-
/// - Returns: An `EventLoopFuture` that will resolve with the Redis command response.
57-
public func command(_ command: String, arguments: [RESPValue] = []) -> EventLoopFuture<RESPValue> {
58-
guard !_isClosed.load() else {
59-
return channel.eventLoop.makeFailedFuture(RedisError.connectionClosed)
60-
}
90+
/// See `RedisCommandExecutor.send(command:with:)`.
91+
public func send(command: String, with arguments: [RESPValueConvertible] = []) -> EventLoopFuture<RESPValue> {
92+
guard !_isClosed.load() else { return channel.eventLoop.makeFailedFuture(RedisError.connectionClosed) }
93+
94+
let args = arguments.map { $0.convertedToRESPValue() }
6195

6296
let promise = channel.eventLoop.makePromise(of: RESPValue.self)
6397
let context = RedisCommandContext(
64-
command: .array([RESPValue(bulk: command)] + arguments),
98+
command: .array([RESPValue(bulk: command)] + args),
6599
promise: promise
66100
)
67101

68102
_ = channel.writeAndFlush(context)
69103

70104
return promise.futureResult
71105
}
72-
73-
/// Creates a `RedisPipeline` for executing a batch of commands.
74-
public func makePipeline() -> RedisPipeline {
75-
return .init(channel: channel)
76-
}
77106
}

Sources/NIORedis/RedisDriver.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,11 @@ public final class RedisDriver {
5151
hostname: String = "localhost",
5252
port: Int = 6379,
5353
password: String? = nil
54-
) -> EventLoopFuture<RedisConnection> {
54+
) -> EventLoopFuture<NIORedisConnection> {
5555
let bootstrap = ClientBootstrap.makeForRedis(using: eventLoopGroup)
5656

5757
return bootstrap.connect(host: hostname, port: port)
58-
.map { return RedisConnection(channel: $0) }
58+
.map { return NIORedisConnection(channel: $0) }
5959
.flatMap { connection in
6060
guard let pw = password else {
6161
return self.eventLoopGroup.next().makeSucceededFuture(connection)

0 commit comments

Comments
 (0)