Skip to content

Commit bdf05d5

Browse files
authored
Command types (#5)
* Command types * fix hello 3 command * writeToRESPBuffer -> encode * Add support for returning maps Makes the assumption the map key is always a string * Enum namespaces for containers * Catch more response arrays in replies * Parameter pack return from pipeline * Update README * Make sure we parse all responses from pipelined requests
1 parent 750aa13 commit bdf05d5

30 files changed

+6883
-8708
lines changed

README.md

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,8 @@ In general you don't need to use this method as it has no advantages over using
5151

5252
```swift
5353
let key = RedisKey(rawValue: "MyKey")
54-
let responses = try await connection.pipeline([
55-
.set(key, "TestString"),
56-
.get(key)
57-
])
54+
let (setResponse, getResponse) = try await connection.pipeline(
55+
SET(key, "TestString"),
56+
GET(key)
57+
)
5858
```
59-
60-
The `RedisConnection.pipeline` command returns an array of `RESPTokens`, one for each command. So we can get the result of the `get` in the above example by converting the second token into a `String`.
61-
62-
```swift
63-
let value = responses[1].converting(to: String.self)
64-
```

Sources/Redis/Connection/RedisConnection.swift

Lines changed: 52 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,12 @@ public struct ServerAddress: Sendable, Equatable {
4444
@_documentation(visibility: internal)
4545
public struct RedisConnection: Sendable {
4646
enum Request {
47-
case command(RESPCommand)
48-
case pipelinedCommands([RESPCommand])
47+
case command(ByteBuffer)
48+
case pipelinedCommands(ByteBuffer, Int)
4949
}
5050
enum Response {
5151
case token(RESPToken)
52-
case pipelinedResponse([RESPToken])
52+
case pipelinedResponse([Result<RESPToken, Error>])
5353
}
5454
typealias RequestStreamElement = (Request, CheckedContinuation<Response, Error>)
5555
/// Logger used by Server
@@ -96,7 +96,7 @@ public struct RedisConnection: Sendable {
9696
do {
9797
switch request {
9898
case .command(let command):
99-
try await outbound.write(command.buffer)
99+
try await outbound.write(command)
100100
let response = try await inboundIterator.next()
101101
if let response {
102102
continuation.resume(returning: .token(response))
@@ -109,22 +109,26 @@ public struct RedisConnection: Sendable {
109109
)
110110
)
111111
}
112-
case .pipelinedCommands(let commands):
113-
try await outbound.write(contentsOf: commands.map { $0.buffer })
114-
var responses: [RESPToken] = .init()
115-
for _ in 0..<commands.count {
116-
let response = try await inboundIterator.next()
117-
if let response {
118-
responses.append(response)
119-
} else {
120-
requestContinuation.finish()
121-
continuation.resume(
122-
throwing: RedisClientError(
123-
.connectionClosed,
124-
message: "The connection to the Redis database was unexpectedly closed."
112+
case .pipelinedCommands(let commands, let count):
113+
try await outbound.write(commands)
114+
var responses: [Result<RESPToken, Error>] = .init()
115+
for _ in 0..<count {
116+
do {
117+
let response = try await inboundIterator.next()
118+
if let response {
119+
responses.append(.success(response))
120+
} else {
121+
requestContinuation.finish()
122+
continuation.resume(
123+
throwing: RedisClientError(
124+
.connectionClosed,
125+
message: "The connection to the Redis database was unexpectedly closed."
126+
)
125127
)
126-
)
127-
return
128+
return
129+
}
130+
} catch {
131+
responses.append(.failure(error))
128132
}
129133
}
130134
continuation.resume(returning: .pipelinedResponse(responses))
@@ -153,14 +157,11 @@ public struct RedisConnection: Sendable {
153157
}
154158
}
155159

156-
@discardableResult public func send(command: RESPCommand) async throws -> RESPToken {
157-
if logger.logLevel <= .debug {
158-
var buffer = command.buffer
159-
let sending = try [String](from: RESPToken(consuming: &buffer)!).joined(separator: " ")
160-
self.logger.debug("send: \(sending)")
161-
}
160+
@discardableResult public func send<Command: RedisCommand>(command: Command) async throws -> Command.Response {
161+
var encoder = RedisCommandEncoder()
162+
command.encode(into: &encoder)
162163
let response: Response = try await withCheckedThrowingContinuation { continuation in
163-
switch requestContinuation.yield((.command(command), continuation)) {
164+
switch requestContinuation.yield((.command(encoder.buffer), continuation)) {
164165
case .enqueued:
165166
break
166167
case .dropped, .terminated:
@@ -175,12 +176,21 @@ public struct RedisConnection: Sendable {
175176
}
176177
}
177178
guard case .token(let token) = response else { preconditionFailure("Expected a single response") }
178-
return token
179+
return try .init(from: token)
179180
}
180181

181-
@discardableResult public func pipeline(_ commands: [RESPCommand]) async throws -> [RESPToken] {
182+
@discardableResult public func pipeline<each Command: RedisCommand>(
183+
_ commands: repeat each Command
184+
) async throws -> (repeat (each Command).Response) {
185+
var count = 0
186+
var encoder = RedisCommandEncoder()
187+
for command in repeat each commands {
188+
command.encode(into: &encoder)
189+
count += 1
190+
}
191+
182192
let response: Response = try await withCheckedThrowingContinuation { continuation in
183-
switch requestContinuation.yield((.pipelinedCommands(commands), continuation)) {
193+
switch requestContinuation.yield((.pipelinedCommands(encoder.buffer, count), continuation)) {
184194
case .enqueued:
185195
break
186196
case .dropped, .terminated:
@@ -195,21 +205,19 @@ public struct RedisConnection: Sendable {
195205
}
196206
}
197207
guard case .pipelinedResponse(let tokens) = response else { preconditionFailure("Expected a single response") }
198-
return tokens
199-
}
200208

201-
@discardableResult public func send<each Arg: RESPRenderable>(_ command: repeat each Arg) async throws -> RESPToken {
202-
let command = RESPCommand(repeat each command)
203-
return try await self.send(command: command)
209+
var index = AutoIncrementingInteger()
210+
return try (repeat (each Command).Response(from: tokens[index.next()].get()))
204211
}
205212

206213
/// Try to upgrade to RESP3
207214
private func resp3Upgrade(
208215
outbound: NIOAsyncChannelOutboundWriter<ByteBuffer>,
209216
inboundIterator: inout NIOAsyncChannelInboundStream<RESPToken>.AsyncIterator
210217
) async throws {
211-
let helloCommand = RESPCommand("HELLO", "3")
212-
try await outbound.write(helloCommand.buffer)
218+
var encoder = RedisCommandEncoder()
219+
encoder.encodeArray("HELLO", 3)
220+
try await outbound.write(encoder.buffer)
213221
let response = try await inboundIterator.next()
214222
guard let response else {
215223
throw RedisClientError(.connectionClosed, message: "The connection to the Redis database was unexpectedly closed.")
@@ -317,3 +325,11 @@ extension ClientBootstrap: ClientBootstrapProtocol {}
317325
#if canImport(Network)
318326
extension NIOTSConnectionBootstrap: ClientBootstrapProtocol {}
319327
#endif
328+
329+
private struct AutoIncrementingInteger {
330+
var value: Int = 0
331+
mutating func next() -> Int {
332+
value += 1
333+
return value - 1
334+
}
335+
}

Sources/Redis/RESP/RESPError.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ public struct RESPParsingError: Error {
7474
public var code: Code
7575
public var buffer: ByteBuffer
7676

77+
@usableFromInline
7778
package init(code: Code, buffer: ByteBuffer) {
7879
self.code = code
7980
self.buffer = buffer

Sources/Redis/RESP/RESPRenderable.swift

Lines changed: 11 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,15 @@ import NIOCore
1616

1717
/// Type that can be rendered into a RESP buffer
1818
public protocol RESPRenderable {
19-
func writeToRESPBuffer(_ buffer: inout ByteBuffer) -> Int
19+
func encode(into commandEncoder: inout RedisCommandEncoder) -> Int
2020
}
2121

2222
extension Optional: RESPRenderable where Wrapped: RESPRenderable {
2323
@inlinable
24-
public func writeToRESPBuffer(_ buffer: inout ByteBuffer) -> Int {
24+
public func encode(into commandEncoder: inout RedisCommandEncoder) -> Int {
2525
switch self {
2626
case .some(let wrapped):
27-
return wrapped.writeToRESPBuffer(&buffer)
27+
return wrapped.encode(into: &commandEncoder)
2828
case .none:
2929
return 0
3030
}
@@ -33,49 +33,35 @@ extension Optional: RESPRenderable where Wrapped: RESPRenderable {
3333

3434
extension Array: RESPRenderable where Element: RESPRenderable {
3535
@inlinable
36-
public func writeToRESPBuffer(_ buffer: inout ByteBuffer) -> Int {
36+
public func encode(into commandEncoder: inout RedisCommandEncoder) -> Int {
3737
var count = 0
3838
for element in self {
39-
count += element.writeToRESPBuffer(&buffer)
39+
count += element.encode(into: &commandEncoder)
4040
}
4141
return count
4242
}
4343
}
4444

4545
extension String: RESPRenderable {
4646
@inlinable
47-
public func writeToRESPBuffer(_ buffer: inout ByteBuffer) -> Int {
48-
buffer.writeBulkString(self)
47+
public func encode(into commandEncoder: inout RedisCommandEncoder) -> Int {
48+
commandEncoder.encodeBulkString(self)
4949
return 1
5050
}
5151
}
5252

5353
extension Int: RESPRenderable {
5454
@inlinable
55-
public func writeToRESPBuffer(_ buffer: inout ByteBuffer) -> Int {
56-
buffer.writeBulkString(String(self))
55+
public func encode(into commandEncoder: inout RedisCommandEncoder) -> Int {
56+
commandEncoder.encodeBulkString(String(self))
5757
return 1
5858
}
5959
}
6060

6161
extension Double: RESPRenderable {
6262
@inlinable
63-
public func writeToRESPBuffer(_ buffer: inout ByteBuffer) -> Int {
64-
buffer.writeBulkString(String(self))
63+
public func encode(into commandEncoder: inout RedisCommandEncoder) -> Int {
64+
commandEncoder.encodeBulkString(String(self))
6565
return 1
6666
}
6767
}
68-
69-
extension ByteBuffer {
70-
public mutating func writeRESP3TypeIdentifier(_ identifier: RESPTypeIdentifier) {
71-
self.writeInteger(identifier.rawValue)
72-
}
73-
74-
public mutating func writeBulkString(_ string: String) {
75-
self.writeRESP3TypeIdentifier(.bulkString)
76-
self.writeString(String(string.utf8.count))
77-
self.writeStaticString("\r\n")
78-
self.writeString(string)
79-
self.writeStaticString("\r\n")
80-
}
81-
}

Sources/Redis/RESP/RESPToken.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ public struct RESPToken: Hashable, Sendable {
9393
case push(Array)
9494
}
9595

96+
@usableFromInline
9697
package let base: ByteBuffer
9798

9899
public var value: Value {

Sources/Redis/RESP/RESPTokenRepresentable.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@ extension RESPToken: RESPTokenRepresentable {
2424
/// - Parameter type: Type to convert to
2525
/// - Throws: RedisClientError.unexpectedType
2626
/// - Returns: Value
27+
@inlinable
2728
public func converting<Value: RESPTokenRepresentable>(to type: Value.Type = Value.self) throws -> Value {
2829
try Value(from: self)
2930
}
3031

32+
@inlinable
3133
public init(from token: RESPToken) throws {
3234
self = token
3335
}
@@ -38,12 +40,14 @@ extension Array where Element == RESPToken {
3840
/// - Parameter type: Type to convert to
3941
/// - Throws: RedisClientError.unexpectedType
4042
/// - Returns: Array of Value
43+
@inlinable
4144
public func converting<Value: RESPTokenRepresentable>(to type: [Value].Type = [Value].self) throws -> [Value] {
4245
try self.map { try $0.converting() }
4346
}
4447
}
4548

4649
extension ByteBuffer: RESPTokenRepresentable {
50+
@inlinable
4751
public init(from token: RESPToken) throws {
4852
switch token.value {
4953
case .simpleString(let buffer), .bulkString(let buffer), .verbatimString(let buffer), .bigNumber(let buffer):
@@ -55,13 +59,15 @@ extension ByteBuffer: RESPTokenRepresentable {
5559
}
5660

5761
extension String: RESPTokenRepresentable {
62+
@inlinable
5863
public init(from token: RESPToken) throws {
5964
let buffer = try ByteBuffer(from: token)
6065
self.init(buffer: buffer)
6166
}
6267
}
6368

6469
extension Int: RESPTokenRepresentable {
70+
@inlinable
6571
public init(from token: RESPToken) throws {
6672
switch token.value {
6773
case .number(let value):
@@ -73,6 +79,7 @@ extension Int: RESPTokenRepresentable {
7379
}
7480

7581
extension Double: RESPTokenRepresentable {
82+
@inlinable
7683
public init(from token: RESPToken) throws {
7784
switch token.value {
7885
case .double(let value):
@@ -84,6 +91,7 @@ extension Double: RESPTokenRepresentable {
8491
}
8592

8693
extension Bool: RESPTokenRepresentable {
94+
@inlinable
8795
public init(from token: RESPToken) throws {
8896
switch token.value {
8997
case .boolean(let value):
@@ -95,6 +103,7 @@ extension Bool: RESPTokenRepresentable {
95103
}
96104

97105
extension Optional: RESPTokenRepresentable where Wrapped: RESPTokenRepresentable {
106+
@inlinable
98107
public init(from token: RESPToken) throws {
99108
switch token.value {
100109
case .null:
@@ -106,6 +115,7 @@ extension Optional: RESPTokenRepresentable where Wrapped: RESPTokenRepresentable
106115
}
107116

108117
extension Array: RESPTokenRepresentable where Element: RESPTokenRepresentable {
118+
@inlinable
109119
public init(from token: RESPToken) throws {
110120
switch token.value {
111121
case .array(let respArray), .push(let respArray):
@@ -122,6 +132,7 @@ extension Array: RESPTokenRepresentable where Element: RESPTokenRepresentable {
122132
}
123133

124134
extension Set: RESPTokenRepresentable where Element: RESPTokenRepresentable {
135+
@inlinable
125136
public init(from token: RESPToken) throws {
126137
switch token.value {
127138
case .set(let respSet):
@@ -138,6 +149,7 @@ extension Set: RESPTokenRepresentable where Element: RESPTokenRepresentable {
138149
}
139150

140151
extension Dictionary: RESPTokenRepresentable where Value: RESPTokenRepresentable, Key: RESPTokenRepresentable {
152+
@inlinable
141153
public init(from token: RESPToken) throws {
142154
switch token.value {
143155
case .map(let respMap), .attribute(let respMap):

Sources/Redis/RedisCommand.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the swift-redis open source project
4+
//
5+
// Copyright (c) 2025 the swift-redis project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of swift-redis project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import NIOCore
16+
17+
/// A redis command that can be executed on a connection.
18+
public protocol RedisCommand {
19+
associatedtype Response: RESPTokenRepresentable = RESPToken
20+
21+
func encode(into commandEncoder: inout RedisCommandEncoder)
22+
}

0 commit comments

Comments
 (0)