Skip to content

Commit ac45bb2

Browse files
authored
RESPDecodeError: Version 2 (#233)
* Add RESPDecodeError, somehow broken arrays though Signed-off-by: Adam Fowler <[email protected]> * Restructure RESPDecodeError to code, message and token Signed-off-by: Adam Fowler <[email protected]> * Add tests for decode errors Signed-off-by: Adam Fowler <[email protected]> * Add expected size to RESPDecodeError.invalidArraySize Signed-off-by: Adam Fowler <[email protected]> * Fix invalid license header in RESPDecodeErrorTests.swift Signed-off-by: Adam Fowler <[email protected]> * Documentation fix Signed-off-by: Adam Fowler <[email protected]> --------- Signed-off-by: Adam Fowler <[email protected]>
1 parent 9b1ec4a commit ac45bb2

13 files changed

+238
-62
lines changed

Sources/Valkey/Commands/Custom/GeoCustomCommands.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ extension GEOSEARCH {
4646
case .array(let array):
4747
var arrayIterator = array.makeIterator()
4848
guard let member = arrayIterator.next() else {
49-
throw RESPParsingError(code: .unexpectedType, buffer: token.base)
49+
throw RESPDecodeError.invalidArraySize(array, expectedSize: 1)
5050
}
5151
self.member = try String(fromRESP: member)
5252
self.attributes = array.dropFirst().map { $0 }
@@ -56,7 +56,7 @@ extension GEOSEARCH {
5656
self.attributes = []
5757

5858
default:
59-
throw RESPParsingError(code: .unexpectedType, buffer: token.base)
59+
throw RESPDecodeError.tokenMismatch(expected: [.array, .bulkString], token: token)
6060
}
6161
}
6262
}

Sources/Valkey/Commands/Custom/ListCustomCommands.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ extension LMPOP {
2525
case .array(let array):
2626
(self.key, self.values) = try array.decodeElements()
2727
default:
28-
throw RESPParsingError(code: .unexpectedType, buffer: token.base)
28+
throw RESPDecodeError.tokenMismatch(expected: [.array], token: token)
2929
}
3030
}
3131
}

Sources/Valkey/Commands/Custom/ServerCustomCommands.swift

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
//
88
extension ROLE {
99
public enum Response: RESPTokenDecodable, Sendable {
10-
struct DecodeError: Error {}
10+
struct MissingValueDecodeError: Error {
11+
let expectedNumberOfValues: Int
12+
}
1113
public struct Primary: Sendable {
1214
public struct Replica: RESPTokenDecodable, Sendable {
1315
public let ip: String
@@ -23,7 +25,7 @@ extension ROLE {
2325

2426
init(arrayIterator: inout RESPToken.Array.Iterator) throws {
2527
guard let replicationOffsetToken = arrayIterator.next(), let replicasToken = arrayIterator.next() else {
26-
throw DecodeError()
28+
throw MissingValueDecodeError(expectedNumberOfValues: 2)
2729
}
2830
self.replicationOffset = try .init(fromRESP: replicationOffsetToken)
2931
self.replicas = try .init(fromRESP: replicasToken)
@@ -39,7 +41,7 @@ extension ROLE {
3941
public init(fromRESP token: RESPToken) throws {
4042
let string = try String(fromRESP: token)
4143
guard let state = State(rawValue: string) else {
42-
throw RESPParsingError(code: .unexpectedType, buffer: token.base)
44+
throw RESPDecodeError(.unexpectedToken, token: token)
4345
}
4446
self = state
4547
}
@@ -55,7 +57,7 @@ extension ROLE {
5557
let stateToken = arrayIterator.next(),
5658
let replicationToken = arrayIterator.next()
5759
else {
58-
throw DecodeError()
60+
throw MissingValueDecodeError(expectedNumberOfValues: 4)
5961
}
6062
self.primaryIP = try .init(fromRESP: primaryIPToken)
6163
self.primaryPort = try .init(fromRESP: primaryPortToken)
@@ -67,7 +69,7 @@ extension ROLE {
6769
public let primaryNames: [String]
6870

6971
init(arrayIterator: inout RESPToken.Array.Iterator) throws {
70-
guard let primaryNamesToken = arrayIterator.next() else { throw DecodeError() }
72+
guard let primaryNamesToken = arrayIterator.next() else { throw MissingValueDecodeError(expectedNumberOfValues: 1) }
7173
self.primaryNames = try .init(fromRESP: primaryNamesToken)
7274
}
7375
}
@@ -81,7 +83,7 @@ extension ROLE {
8183
do {
8284
var iterator = array.makeIterator()
8385
guard let roleToken = iterator.next() else {
84-
throw RESPParsingError(code: .unexpectedType, buffer: token.base)
86+
throw RESPDecodeError.invalidArraySize(array, expectedSize: 1)
8587
}
8688
let role = try String(fromRESP: roleToken)
8789
switch role {
@@ -95,13 +97,13 @@ extension ROLE {
9597
let sentinel = try Sentinel(arrayIterator: &iterator)
9698
self = .sentinel(sentinel)
9799
default:
98-
throw DecodeError()
100+
throw RESPDecodeError(.unexpectedToken, token: token)
99101
}
100-
} catch {
101-
throw RESPParsingError(code: .unexpectedType, buffer: token.base)
102+
} catch let error as MissingValueDecodeError {
103+
throw RESPDecodeError.invalidArraySize(array, expectedSize: error.expectedNumberOfValues + 1)
102104
}
103105
default:
104-
throw RESPParsingError(code: .unexpectedType, buffer: token.base)
106+
throw RESPDecodeError.tokenMismatch(expected: [.array], token: token)
105107
}
106108
}
107109
}

Sources/Valkey/Commands/Custom/SetCustomCommands.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,7 @@ extension SSCAN {
1212

1313
public init(fromRESP token: RESPToken) throws {
1414
// cursor is encoded as a bulkString, but should be
15-
let (cursorString, elements) = try token.decodeArrayElements(as: (String, RESPToken.Array).self)
16-
guard let cursor = Int(cursorString) else { throw RESPParsingError(code: .unexpectedType, buffer: token.base) }
15+
let (cursor, elements) = try token.decodeArrayElements(as: (Int, RESPToken.Array).self)
1716
self.cursor = cursor
1817
self.elements = elements
1918
}

Sources/Valkey/Commands/Custom/SortedSetCustomCommands.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public struct SortedSetEntry: RESPTokenDecodable, Sendable {
1818
case .array(let array):
1919
(self.value, self.score) = try array.decodeElements()
2020
default:
21-
throw RESPParsingError(code: .unexpectedType, buffer: token.base)
21+
throw RESPDecodeError.tokenMismatch(expected: [.array], token: token)
2222
}
2323
}
2424
}
@@ -56,7 +56,7 @@ extension ZMPOP {
5656
case .array(let array):
5757
(self.key, self.values) = try array.decodeElements()
5858
default:
59-
throw RESPParsingError(code: .unexpectedType, buffer: token.base)
59+
throw RESPDecodeError.tokenMismatch(expected: [.array], token: token)
6060
}
6161
}
6262
}

Sources/Valkey/Commands/Custom/StreamCustomCommands.swift

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public struct XREADMessage: RESPTokenDecodable, Sendable {
2121
self.id = id
2222
self.fields = keyValuePairs
2323
default:
24-
throw RESPParsingError(code: .unexpectedType, buffer: token.base)
24+
throw RESPDecodeError.tokenMismatch(expected: [.array], token: token)
2525
}
2626
}
2727

@@ -77,7 +77,7 @@ public struct XREADGroupMessage: RESPTokenDecodable, Sendable {
7777
self.id = id
7878
self.fields = keyValuePairs
7979
default:
80-
throw RESPParsingError(code: .unexpectedType, buffer: token.base)
80+
throw RESPDecodeError.tokenMismatch(expected: [.array], token: token)
8181
}
8282
}
8383
}
@@ -100,7 +100,7 @@ public struct XREADStreams<Message>: RESPTokenDecodable, Sendable where Message:
100100
return Stream(key: key, messages: messages)
101101
}
102102
default:
103-
throw RESPParsingError(code: .unexpectedType, buffer: token.base)
103+
throw RESPDecodeError.tokenMismatch(expected: [.map], token: token)
104104
}
105105
}
106106
}
@@ -116,7 +116,7 @@ public struct XAUTOCLAIMResponse: RESPTokenDecodable, Sendable {
116116
case .array(let array):
117117
(self.streamID, self.messages, self.deletedMessages) = try array.decodeElements()
118118
default:
119-
throw RESPParsingError(code: .unexpectedType, buffer: token.base)
119+
throw RESPDecodeError.tokenMismatch(expected: [.array], token: token)
120120
}
121121
}
122122
}
@@ -143,7 +143,7 @@ public enum XCLAIMResponse: RESPTokenDecodable, Sendable {
143143
self = try .ids(array.decode())
144144
}
145145
default:
146-
throw RESPParsingError(code: .unexpectedType, buffer: token.base)
146+
throw RESPDecodeError.tokenMismatch(expected: [.array], token: token)
147147
}
148148
}
149149
}
@@ -164,7 +164,7 @@ public enum XPENDINGResponse: RESPTokenDecodable, Sendable {
164164
case .array(let array):
165165
(self.consumer, self.count) = try array.decodeElements()
166166
default:
167-
throw RESPParsingError(code: .unexpectedType, buffer: token.base)
167+
throw RESPDecodeError.tokenMismatch(expected: [.array], token: token)
168168
}
169169
}
170170
}
@@ -178,7 +178,7 @@ public enum XPENDINGResponse: RESPTokenDecodable, Sendable {
178178
case .array(let array):
179179
(self.pendingMessageCount, self.minimumID, self.maximumID, self.consumers) = try array.decodeElements()
180180
default:
181-
throw RESPParsingError(code: .unexpectedType, buffer: token.base)
181+
throw RESPDecodeError.tokenMismatch(expected: [.array], token: token)
182182
}
183183
}
184184
}
@@ -194,7 +194,7 @@ public enum XPENDINGResponse: RESPTokenDecodable, Sendable {
194194
case .array(let array):
195195
(self.id, self.consumer, self.millisecondsSinceDelivered, self.numberOfTimesDelivered) = try array.decodeElements()
196196
default:
197-
throw RESPParsingError(code: .unexpectedType, buffer: token.base)
197+
throw RESPDecodeError.tokenMismatch(expected: [.array], token: token)
198198
}
199199
}
200200
}
@@ -205,7 +205,7 @@ public enum XPENDINGResponse: RESPTokenDecodable, Sendable {
205205
case .array(let array):
206206
self.messages = try array.decode(as: [PendingMessage].self)
207207
default:
208-
throw RESPParsingError(code: .unexpectedType, buffer: token.base)
208+
throw RESPDecodeError.tokenMismatch(expected: [.array], token: token)
209209
}
210210
}
211211
}

Sources/Valkey/Commands/Custom/StringCustomCommands.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,12 @@ extension LCS {
4242
default: break
4343
}
4444
}
45-
guard let matches else { throw RESPParsingError(code: .unexpectedType, buffer: token.base) }
46-
guard let length else { throw RESPParsingError(code: .unexpectedType, buffer: token.base) }
45+
guard let matches else { throw RESPDecodeError.missingToken(key: "matches", token: token) }
46+
guard let length else { throw RESPDecodeError.missingToken(key: "length", token: token) }
4747
self = .matches(length: numericCast(length), matches: matches)
4848
default:
49-
throw RESPParsingError(code: .unexpectedType, buffer: token.base)
49+
throw RESPDecodeError.tokenMismatch(expected: [.bulkString, .integer, .map], token: token)
50+
5051
}
5152
}
5253
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
//
2+
// This source file is part of the valkey-swift project
3+
// Copyright (c) 2025 the valkey-swift project authors
4+
//
5+
// See LICENSE.txt for license information
6+
// SPDX-License-Identifier: Apache-2.0
7+
//
8+
/// Error returned when decoding a RESPToken.
9+
/// Error thrown when decoding RESPTokens
10+
public struct RESPDecodeError: Error {
11+
/// Error code for decode error
12+
public struct ErrorCode: Sendable, Equatable, CustomStringConvertible {
13+
fileprivate enum Code: Sendable, Equatable {
14+
case tokenMismatch
15+
case invalidArraySize
16+
case missingToken
17+
case cannotParseInteger
18+
case cannotParseDouble
19+
case unexpectedToken
20+
}
21+
22+
fileprivate let code: Code
23+
fileprivate init(_ code: Code) {
24+
self.code = code
25+
}
26+
27+
public var description: String { String(describing: self.code) }
28+
29+
/// Token does not match one of the expected tokens
30+
public static var tokenMismatch: Self { .init(.tokenMismatch) }
31+
/// Does not match the expected array size
32+
public static var invalidArraySize: Self { .init(.invalidArraySize) }
33+
/// Token is missing
34+
public static var missingToken: Self { .init(.missingToken) }
35+
/// Failed to parse an integer
36+
public static var cannotParseInteger: Self { .init(.cannotParseInteger) }
37+
/// Failed to parse a double
38+
public static var cannotParseDouble: Self { .init(.cannotParseDouble) }
39+
/// Token is not as expected
40+
public static var unexpectedToken: Self { .init(.unexpectedToken) }
41+
}
42+
public let errorCode: ErrorCode
43+
public let message: String?
44+
public let token: RESPToken.Value
45+
46+
public init(_ errorCode: ErrorCode, token: RESPToken.Value, message: String? = nil) {
47+
self.errorCode = errorCode
48+
self.token = token
49+
self.message = message
50+
}
51+
52+
public init(_ errorCode: ErrorCode, token: RESPToken, message: String? = nil) {
53+
self = .init(errorCode, token: token.value, message: message)
54+
}
55+
56+
/// Token does not match one of the expected tokens
57+
public static func tokenMismatch(expected: [RESPTypeIdentifier], token: RESPToken) -> Self {
58+
if expected.count == 0 {
59+
return .init(.tokenMismatch, token: token, message: "Found unexpected token while decoding")
60+
} else if expected.count == 1 {
61+
return .init(.tokenMismatch, token: token, message: "Expected to find a \(expected[0])")
62+
} else {
63+
let expectedTokens = "\(expected.dropLast().map { "\($0)" }.joined(separator: ", ")) or \(expected.last!)"
64+
return .init(.tokenMismatch, token: token, message: "Expected to find a \(expectedTokens) token")
65+
}
66+
}
67+
/// Does not match the expected array size
68+
public static func invalidArraySize(_ array: RESPToken.Array, expectedSize: Int) -> Self {
69+
.init(
70+
.invalidArraySize,
71+
token: .array(array),
72+
message: "Expected array of size \(expectedSize) but got an array of size \(array.count)"
73+
)
74+
}
75+
/// Token associated with key is missing
76+
public static func missingToken(key: String, token: RESPToken) -> Self {
77+
.init(.missingToken, token: token, message: "Expected map to contain token with key \"\(key)\"")
78+
}
79+
}
80+
81+
extension RESPDecodeError: CustomStringConvertible {
82+
public var description: String {
83+
"Error: \"\(self.message ?? String(describing: self.errorCode))\", token: \(self.token.debugDescription)"
84+
}
85+
}

0 commit comments

Comments
 (0)