Skip to content

Commit 19cbadc

Browse files
committed
Clarify and make public RESPDecoder
1 parent e01cdf5 commit 19cbadc

File tree

2 files changed

+56
-50
lines changed

2 files changed

+56
-50
lines changed

Sources/NIORedis/ChannelHandlers/RESPDecoder.swift

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

4-
/// Handles incoming byte messages from Redis and decodes them according to the RESP protocol.
5-
///
6-
/// See: https://redis.io/topics/protocol
7-
internal final class RESPDecoder: ByteToMessageDecoder {
8-
/// `ByteToMessageDecoder`
9-
public typealias InboundOut = RESPValue
10-
11-
/// See `ByteToMessageDecoder.decode(ctx:buffer:)`
12-
func decode(ctx: ChannelHandlerContext, buffer: inout ByteBuffer) throws -> DecodingState {
13-
var position = 0
14-
15-
switch try _parse(at: &position, from: &buffer) {
16-
case .notYetParsed:
17-
return .needMoreData
18-
19-
case .parsed(let RESPValue):
20-
ctx.fireChannelRead(wrapInboundOut(RESPValue))
21-
buffer.moveReaderIndex(forwardBy: position)
22-
return .continue
23-
}
24-
}
25-
}
26-
27-
// MARK: RESP Parsing
28-
294
extension UInt8 {
305
static let newline: UInt8 = 0xA
316
static let carriageReturn: UInt8 = 0xD
@@ -36,13 +11,39 @@ extension UInt8 {
3611
static let colon: UInt8 = 0x3A
3712
}
3813

39-
extension RESPDecoder {
40-
enum _RESPValueDecodingState {
14+
private extension ByteBuffer {
15+
/// Copies bytes from the `ByteBuffer` from at the provided position, up to the length desired.
16+
///
17+
/// buffer.copyBytes(at: 3, length: 2)
18+
/// // Optional(2 bytes), assuming buffer contains 5 bytes
19+
///
20+
/// - Parameters:
21+
/// - at: The position offset to copy bytes from the buffer, defaulting to `0`.
22+
/// - length: The number of bytes to copy.
23+
func copyBytes(at offset: Int = 0, length: Int) -> [UInt8]? {
24+
guard readableBytes >= offset + length else { return nil }
25+
return getBytes(at: offset + readerIndex, length: length)
26+
}
27+
}
28+
29+
/// Handles incoming byte messages from Redis and decodes them according to the RESP protocol.
30+
///
31+
/// See: https://redis.io/topics/protocol
32+
public final class RESPDecoder {
33+
/// Representation of a `RESPDecoder.parse(at:from:) result, with either a decoded `RESPValue` or an indicator
34+
/// that the buffer contains an incomplete RESP message from the position provided.
35+
public enum ParsingState {
4136
case notYetParsed
4237
case parsed(RESPValue)
4338
}
4439

45-
func _parse(at position: inout Int, from buffer: inout ByteBuffer) throws -> _RESPValueDecodingState {
40+
/// Attempts to parse the `ByteBuffer`, starting at the specified position, following the RESP specification.
41+
///
42+
/// See https://redis.io/topics/protocol
43+
/// - Parameters:
44+
/// - at: The index of the buffer that should be considered the "front" to begin message parsing.
45+
/// - from: The buffer that contains the bytes that need to be decoded.
46+
public func parse(at position: inout Int, from buffer: inout ByteBuffer) throws -> ParsingState {
4647
guard let token = buffer.copyBytes(at: position, length: 1)?.first else { return .notYetParsed }
4748

4849
position += 1
@@ -120,7 +121,7 @@ extension RESPDecoder {
120121
}
121122

122123
/// See https://redis.io/topics/protocol#resp-bulk-strings
123-
func _parseBulkString(at position: inout Int, from buffer: inout ByteBuffer) throws -> _RESPValueDecodingState {
124+
func _parseBulkString(at position: inout Int, from buffer: inout ByteBuffer) throws -> ParsingState {
124125
guard let size = try _parseInteger(at: &position, from: &buffer) else { return .notYetParsed }
125126

126127
// Redis sends '-1' to represent a null string
@@ -153,16 +154,16 @@ extension RESPDecoder {
153154
}
154155

155156
/// See https://redis.io/topics/protocol#resp-arrays
156-
func _parseArray(at position: inout Int, from buffer: inout ByteBuffer) throws -> _RESPValueDecodingState {
157+
func _parseArray(at position: inout Int, from buffer: inout ByteBuffer) throws -> ParsingState {
157158
guard let arraySize = try _parseInteger(at: &position, from: &buffer) else { return .notYetParsed }
158159
guard arraySize > -1 else { return .parsed(.null) }
159160
guard arraySize > 0 else { return .parsed(.array([])) }
160161

161-
var array = [_RESPValueDecodingState](repeating: .notYetParsed, count: arraySize)
162+
var array = [ParsingState](repeating: .notYetParsed, count: arraySize)
162163
for index in 0..<arraySize {
163164
guard buffer.readableBytes - position > 0 else { return .notYetParsed }
164165

165-
let parseResult = try _parse(at: &position, from: &buffer)
166+
let parseResult = try parse(at: &position, from: &buffer)
166167
switch parseResult {
167168
case .parsed:
168169
array[index] = parseResult
@@ -184,17 +185,22 @@ extension RESPDecoder {
184185
}
185186
}
186187

187-
private extension ByteBuffer {
188-
/// Copies bytes from the `ByteBuffer` from at the provided position, up to the length desired.
189-
///
190-
/// buffer.copyBytes(at: 3, length: 2)
191-
/// // Optional(2 bytes), assuming buffer contains 5 bytes
192-
///
193-
/// - Parameters:
194-
/// - at: The position offset to copy bytes from the buffer, defaulting to `0`.
195-
/// - length: The number of bytes to copy.
196-
func copyBytes(at offset: Int = 0, length: Int) -> [UInt8]? {
197-
guard readableBytes >= offset + length else { return nil }
198-
return getBytes(at: offset + readerIndex, length: length)
188+
extension RESPDecoder: ByteToMessageDecoder {
189+
/// `ByteToMessageDecoder.InboundOut`
190+
public typealias InboundOut = RESPValue
191+
192+
/// See `ByteToMessageDecoder.decode(ctx:buffer:)`
193+
public func decode(ctx: ChannelHandlerContext, buffer: inout ByteBuffer) throws -> DecodingState {
194+
var position = 0
195+
196+
switch try parse(at: &position, from: &buffer) {
197+
case .notYetParsed:
198+
return .needMoreData
199+
200+
case .parsed(let RESPValue):
201+
ctx.fireChannelRead(wrapInboundOut(RESPValue))
202+
buffer.moveReaderIndex(forwardBy: position)
203+
return .continue
204+
}
199205
}
200206
}

Tests/NIORedisTests/ChannelHandlers/RESPDecoder+ParsingTests.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,8 @@ final class RESPDecoderParsingTests: XCTestCase {
9292
_ = runParse(offset: 0) { decoder, position, buffer in
9393
buffer.write(string: "&3\r\n")
9494
do {
95-
_ = try decoder._parse(at: &position, from: &buffer)
96-
XCTFail("_parse(at:from:) did not throw an expected error!")
95+
_ = try decoder.parse(at: &position, from: &buffer)
96+
XCTFail("parse(at:from:) did not throw an expected error!")
9797
}
9898
catch { XCTAssertTrue(error is RedisError) }
9999

@@ -107,7 +107,7 @@ final class RESPDecoderParsingTests: XCTestCase {
107107
let result = runParse(offset: 0) { decoder, position, buffer in
108108
buffer.write(string: testString)
109109
guard
110-
case .parsed(let data) = try decoder._parse(at: &position, from: &buffer),
110+
case .parsed(let data) = try decoder.parse(at: &position, from: &buffer),
111111
case .error = data
112112
else { return nil }
113113

@@ -126,7 +126,7 @@ final class RESPDecoderParsingTests: XCTestCase {
126126
private func parseTest_singleValue(input: Data) -> RESPValue? {
127127
return runParse(offset: 0) { decoder, position, buffer in
128128
buffer.write(bytes: input)
129-
guard case .parsed(let result)? = try? decoder._parse(at: &position, from: &buffer) else { return nil }
129+
guard case .parsed(let result)? = try? decoder.parse(at: &position, from: &buffer) else { return nil }
130130
return result
131131
}
132132
}
@@ -148,8 +148,8 @@ final class RESPDecoderParsingTests: XCTestCase {
148148
}
149149

150150
var position = 0
151-
let p1 = try decoder._parse(at: &position, from: &buffer)
152-
let p2 = try decoder._parse(at: &position, from: &buffer)
151+
let p1 = try decoder.parse(at: &position, from: &buffer)
152+
let p2 = try decoder.parse(at: &position, from: &buffer)
153153

154154
guard
155155
case .parsed(let first) = p1,

0 commit comments

Comments
 (0)