Skip to content

Commit aa185a0

Browse files
committedDec 26, 2022
Lay groundwork for RESP3 support and flat ChannelHandler hierarchy
## Motivation Since Redis 6.0, a new serialization protocol format (v3) is available that gives richer semantic reasoning behind the different types to enable commands to better understand the return types to provide in their programming language. In addition, the `RESPTranslator` type is going to see more direct usage, and the current API doesn't make read well. ## Changes - Add: Internal `RESPVersion` enum that the `RESPTranslator` will start to use - Rename: `RESPTranslator.parseBytes` to `RESPTranslator.read(from:)`
1 parent 3f7fedb commit aa185a0

File tree

3 files changed

+95
-74
lines changed

3 files changed

+95
-74
lines changed
 

‎Sources/RediStack/ChannelHandlers/RedisByteDecoder.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public struct RedisByteDecoder: NIOSingleStepByteToMessageDecoder {
3030

3131
/// See `NIOSingleStepByteToMessageDecoder.decode(buffer:)`
3232
public func decode(buffer: inout ByteBuffer) throws -> RESPValue? {
33-
try self.parser.parseBytes(from: &buffer)
33+
try self.parser.read(from: &buffer)
3434
}
3535

3636
/// See `NIOSingleStepByteToMessageDecoder.decodeLast(buffer:seenEOF)`

‎Sources/RediStack/RESP/RESPTranslator.swift

+89-68
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,96 @@
1515
import protocol Foundation.LocalizedError
1616
import NIOCore
1717

18+
extension UInt8 {
19+
static let newline = UInt8(ascii: "\n")
20+
static let carriageReturn = UInt8(ascii: "\r")
21+
static let dollar = UInt8(ascii: "$")
22+
static let asterisk = UInt8(ascii: "*")
23+
static let plus = UInt8(ascii: "+")
24+
static let hyphen = UInt8(ascii: "-")
25+
static let colon = UInt8(ascii: ":")
26+
}
27+
28+
// This is not ready for prime-time
29+
30+
/// An exhaustive list of the available versions of the Redis Serialization Protocol.
31+
/// - Warning: These values are not generally intended to be used outside of this library,
32+
/// so no guarantees to source stability are given.
33+
fileprivate enum RESPVersion {
34+
/// The RESP version first made available in Redis 1.2.
35+
///
36+
/// It was made the default version in Redis 2.0.
37+
case v2
38+
/// The RESP version first made available in Redis 6.0.
39+
case v3
40+
}
41+
42+
extension RESPTranslator {
43+
/// Possible errors thrown while parsing RESP messages.
44+
/// - Important: Any of these errors should be considered a **BUG**.
45+
///
46+
/// Please file a bug at [https://www.gitlab.com/swift-server-community/RediStack/-/issues](https://www.gitlab.com/swift-server-community/RediStack/-/issues).
47+
public struct ParsingError: LocalizedError, Equatable {
48+
/// An invalid RESP data type identifier was found.
49+
public static let invalidToken = ParsingError(.invalidToken)
50+
/// A bulk string size did not match the RESP schema.
51+
public static let invalidBulkStringSize = ParsingError(.invalidBulkStringSize)
52+
/// A bulk string's declared size did not match its content size.
53+
public static let bulkStringSizeMismatch = ParsingError(.bulkStringSizeMismatch)
54+
/// A RESP integer did not follow the RESP schema.
55+
public static let invalidIntegerFormat = ParsingError(.invalidIntegerFormat)
56+
57+
public var errorDescription: String? {
58+
return self.base.rawValue
59+
}
60+
61+
private let base: Base
62+
private init(_ base: Base) { self.base = base }
63+
64+
private enum Base: String, Equatable {
65+
case invalidToken = "Cannot parse RESP: invalid token"
66+
case invalidBulkStringSize = "Cannot parse RESP Bulk String: received invalid size"
67+
case bulkStringSizeMismatch = "Cannot parse RESP Bulk String: declared size and content size do not match"
68+
case invalidIntegerFormat = "Cannot parse RESP Integer: invalid integer format"
69+
}
70+
}
71+
}
72+
1873
/// A helper object for translating between raw bytes and Swift types according to the Redis Serialization Protocol (RESP).
1974
///
2075
/// See [https://redis.io/topics/protocol](https://redis.io/topics/protocol)
2176
public struct RESPTranslator {
22-
public init() { }
77+
private let version: RESPVersion
78+
79+
public init() {
80+
self.version = .v2
81+
}
82+
83+
/// Attempts to read a complete `RESPValue` from the `ByteBuffer`.
84+
/// - Important: The provided `buffer` will have its reader index moved on a successful read.
85+
/// - Throws:
86+
/// - `RESPTranslator.ParsingError.invalidToken` if the first byte is not an expected RESP Data Type token.
87+
/// - Parameter buffer: The buffer that contains the bytes that need to be parsed.
88+
/// - Returns: The parsed `RESPValue` or nil.
89+
public func read(from buffer: inout ByteBuffer) throws -> RESPValue? {
90+
return try self.parseBytesV2(from: &buffer)
91+
}
92+
93+
/// Writes the value into the desired `ByteBuffer` in RESP format.
94+
/// - Parameters:
95+
/// - value: The value to write into the buffer.
96+
/// - out: The `ByteBuffer` that should be written to.
97+
@inlinable
98+
public func write<Value: RESPValueConvertible>(_ value: Value, into out: inout ByteBuffer) {
99+
out.writeRESPValue(value.convertedToRESPValue())
100+
}
23101
}
24102

25103
// MARK: Writing RESP
26104

27105
/// The carriage return and newline escape symbols, used as the standard signal in RESP for a "message" end.
28106
/// A "message" in this case is a single data type.
29-
fileprivate let respEnd: StaticString = "\r\n"
107+
fileprivate let kSegmentEnd: StaticString = "\r\n"
30108

31109
extension ByteBuffer {
32110
/// Writes the `RESPValue` into the current buffer, following the RESP specification.
@@ -38,101 +116,44 @@ extension ByteBuffer {
38116
case .simpleString(var buffer):
39117
self.writeStaticString("+")
40118
self.writeBuffer(&buffer)
41-
self.writeStaticString(respEnd)
119+
self.writeStaticString(kSegmentEnd)
42120

43121
case .bulkString(.some(var buffer)):
44122
self.writeStaticString("$")
45123
self.writeString("\(buffer.readableBytes)")
46-
self.writeStaticString(respEnd)
124+
self.writeStaticString(kSegmentEnd)
47125
self.writeBuffer(&buffer)
48-
self.writeStaticString(respEnd)
126+
self.writeStaticString(kSegmentEnd)
49127

50128
case .bulkString(.none):
51129
self.writeStaticString("$0\r\n\r\n")
52130

53131
case .integer(let number):
54132
self.writeStaticString(":")
55133
self.writeString(number.description)
56-
self.writeStaticString(respEnd)
134+
self.writeStaticString(kSegmentEnd)
57135

58136
case .null:
59137
self.writeStaticString("$-1\r\n")
60138

61139
case .error(let error):
62140
self.writeStaticString("-")
63141
self.writeString(error.message)
64-
self.writeStaticString(respEnd)
142+
self.writeStaticString(kSegmentEnd)
65143

66144
case .array(let array):
67145
self.writeStaticString("*")
68146
self.writeString("\(array.count)")
69-
self.writeStaticString(respEnd)
147+
self.writeStaticString(kSegmentEnd)
70148
array.forEach { self.writeRESPValue($0) }
71149
}
72150
}
73151
}
74152

75-
extension RESPTranslator {
76-
/// Writes the value into the desired `ByteBuffer` in RESP format.
77-
/// - Parameters:
78-
/// - value: The value to write into the buffer.
79-
/// - out: The `ByteBuffer` that should be written to.
80-
public func write<Value: RESPValueConvertible>(_ value: Value, into out: inout ByteBuffer) {
81-
out.writeRESPValue(value.convertedToRESPValue())
82-
}
83-
}
84-
85-
// MARK: Reading RESP
86-
87-
extension UInt8 {
88-
static let newline = UInt8(ascii: "\n")
89-
static let carriageReturn = UInt8(ascii: "\r")
90-
static let dollar = UInt8(ascii: "$")
91-
static let asterisk = UInt8(ascii: "*")
92-
static let plus = UInt8(ascii: "+")
93-
static let hyphen = UInt8(ascii: "-")
94-
static let colon = UInt8(ascii: ":")
95-
}
153+
// MARK: V2 Parsing
96154

97155
extension RESPTranslator {
98-
/// Possible errors thrown while parsing RESP messages.
99-
/// - Important: Any of these errors should be considered a **BUG**.
100-
///
101-
/// Please file a bug at [https://www.gitlab.com/mordil/RediStack/-/issues](https://www.gitlab.com/mordil/RediStack/-/issues).
102-
public struct ParsingError: LocalizedError, Equatable {
103-
/// An invalid RESP data type identifier was found.
104-
public static let invalidToken = ParsingError(.invalidToken)
105-
/// A bulk string size did not match the RESP schema.
106-
public static let invalidBulkStringSize = ParsingError(.invalidBulkStringSize)
107-
/// A bulk string's declared size did not match its content size.
108-
public static let bulkStringSizeMismatch = ParsingError(.bulkStringSizeMismatch)
109-
/// A RESP integer did not follow the RESP schema.
110-
public static let invalidIntegerFormat = ParsingError(.invalidIntegerFormat)
111-
112-
public var errorDescription: String? {
113-
return self.base.rawValue
114-
}
115-
116-
private let base: Base
117-
private init(_ base: Base) { self.base = base }
118-
119-
private enum Base: String, Equatable {
120-
case invalidToken = "Cannot parse RESP: invalid token"
121-
case invalidBulkStringSize = "Cannot parse RESP Bulk String: received invalid size"
122-
case bulkStringSizeMismatch = "Cannot parse RESP Bulk String: declared size and content size do not match"
123-
case invalidIntegerFormat = "Cannot parse RESP Integer: invalid integer format"
124-
}
125-
}
126-
}
127-
128-
extension RESPTranslator {
129-
/// Attempts to parse a `RESPValue` from the `ByteBuffer`.
130-
/// - Important: The provided `buffer` will have its reader index moved on a successful parse.
131-
/// - Throws:
132-
/// - `RESPTranslator.ParsingError.invalidToken` if the first byte is not an expected RESP Data Type token.
133-
/// - Parameter buffer: The buffer that contains the bytes that need to be parsed.
134-
/// - Returns: The parsed `RESPValue` or nil.
135-
public func parseBytes(from buffer: inout ByteBuffer) throws -> RESPValue? {
156+
private func parseBytesV2(from buffer: inout ByteBuffer) throws -> RESPValue? {
136157
var copy = buffer
137158

138159
guard let token = copy.readInteger(as: UInt8.self) else { return nil }
@@ -253,7 +274,7 @@ extension RESPTranslator {
253274

254275
for _ in 0..<elementCount {
255276
guard buffer.readableBytes > 0 else { return nil }
256-
guard let element = try self.parseBytes(from: &buffer) else { return nil }
277+
guard let element = try self.read(from: &buffer) else { return nil }
257278
results.append(element)
258279
}
259280

‎Tests/RediStackTests/RESPTranslatorTests.swift

+5-5
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ extension RESPTranslatorTests {
108108
func testParsing_invalidToken() {
109109
var buffer = self.allocator.buffer(capacity: 128)
110110
buffer.writeString("!!!!")
111-
XCTAssertThrowsError(try self.parser.parseBytes(from: &buffer)) { error in
111+
XCTAssertThrowsError(try self.parser.read(from: &buffer)) { error in
112112
XCTAssertEqual(error as? RESPTranslator.ParsingError, .invalidToken)
113113
}
114114
}
@@ -118,7 +118,7 @@ extension RESPTranslatorTests {
118118
var buffer = allocator.buffer(capacity: testRESP.count)
119119
buffer.writeString(testRESP)
120120

121-
XCTAssertThrowsError(try parser.parseBytes(from: &buffer))
121+
XCTAssertThrowsError(try parser.read(from: &buffer))
122122
XCTAssertEqual(buffer.readerIndex, 0)
123123
}
124124

@@ -211,7 +211,7 @@ extension RESPTranslatorTests {
211211
var buffer = allocator.buffer(capacity: expectedContent.count)
212212
buffer.writeString(testString)
213213

214-
guard let value = try parser.parseBytes(from: &buffer) else { return XCTFail("Failed to parse error") }
214+
guard let value = try parser.read(from: &buffer) else { return XCTFail("Failed to parse error") }
215215

216216
XCTAssertEqual(value.error?.message.contains(expectedContent), true)
217217
}
@@ -224,7 +224,7 @@ extension RESPTranslatorTests {
224224
var buffer = allocator.buffer(capacity: input.count)
225225
buffer.writeBytes(input)
226226

227-
let result = try parser.parseBytes(from: &buffer)
227+
let result = try parser.read(from: &buffer)
228228
assert(buffer.readerIndex == buffer.writerIndex)
229229

230230
return result
@@ -242,7 +242,7 @@ extension RESPTranslatorTests {
242242
for chunk in messageChunks {
243243
buffer.writeBytes(chunk)
244244

245-
guard let result = try parser.parseBytes(from: &buffer) else { continue }
245+
guard let result = try parser.read(from: &buffer) else { continue }
246246

247247
results.append(result)
248248
}

0 commit comments

Comments
 (0)
Please sign in to comment.