Skip to content

Commit e64249b

Browse files
authored
Replace ValkeyClusterParseError with RESPDecodeError (#265)
1 parent 99297c2 commit e64249b

File tree

3 files changed

+60
-71
lines changed

3 files changed

+60
-71
lines changed

Sources/Valkey/Commands/Custom/ClusterCustomCommands.swift

Lines changed: 45 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -35,29 +35,6 @@ extension CLUSTER.SLOTS {
3535
public typealias Response = [ValkeyClusterSlotRange]
3636
}
3737

38-
package struct ValkeyClusterParseError: Error, Equatable {
39-
package enum Reason: Error {
40-
case clusterDescriptionTokenIsNotAnArray
41-
case shardTokenIsNotAnArrayOrMap
42-
case nodesTokenIsNotAnArray
43-
case nodeTokenIsNotAnArrayOrMap
44-
case slotsTokenIsNotAnArray
45-
case invalidNodeRole
46-
case invalidNodeHealth
47-
case missingRequiredValueForNode
48-
case shardIsMissingHashSlots
49-
case shardIsMissingNode
50-
}
51-
52-
package var reason: Reason
53-
package var token: RESPToken
54-
55-
package init(reason: Reason, token: RESPToken) {
56-
self.reason = reason
57-
self.token = token
58-
}
59-
}
60-
6138
/// A description of a Valkey cluster.
6239
///
6340
/// A description is return when you call ``ValkeyClientProtocol/clusterShards()``.
@@ -204,11 +181,7 @@ public struct ValkeyClusterDescription: Hashable, Sendable, RESPTokenDecodable {
204181
/// Creates a cluster description from the response token you provide.
205182
/// - Parameter respToken: The response token.
206183
public init(fromRESP respToken: RESPToken) throws {
207-
do {
208-
self = try Self.makeClusterDescription(respToken: respToken)
209-
} catch {
210-
throw ValkeyClusterParseError(reason: error, token: respToken)
211-
}
184+
self = try Self.makeClusterDescription(respToken: respToken)
212185
}
213186

214187
/// Creates a cluster description from a list of shards you provide.
@@ -611,28 +584,21 @@ public struct ValkeyClusterSlotRange: Hashable, Sendable, RESPTokenDecodable {
611584
}
612585

613586
extension ValkeyClusterDescription {
614-
fileprivate static func makeClusterDescription(respToken: RESPToken) throws(ValkeyClusterParseError.Reason) -> ValkeyClusterDescription {
587+
fileprivate static func makeClusterDescription(respToken: RESPToken) throws(RESPDecodeError) -> ValkeyClusterDescription {
615588
guard case .array(let shardsToken) = respToken.value else {
616-
throw .clusterDescriptionTokenIsNotAnArray
589+
throw RESPDecodeError.tokenMismatch(expected: [.array], token: respToken)
617590
}
618-
let shards = try shardsToken.map { shardToken throws(ValkeyClusterParseError.Reason) in
591+
let shards = try shardsToken.map { shardToken throws(RESPDecodeError) in
619592
try ValkeyClusterDescription.Shard(shardToken)
620593
}
621594
return ValkeyClusterDescription(shards)
622595
}
623596
}
624597

625598
extension HashSlots {
626-
fileprivate init(_ iterator: inout RESPToken.Array.Iterator) throws(ValkeyClusterParseError.Reason) {
627-
guard let token = iterator.next() else {
628-
throw .slotsTokenIsNotAnArray
629-
}
630-
self = try HashSlots(token)
631-
}
632-
633-
fileprivate init(_ token: RESPToken) throws(ValkeyClusterParseError.Reason) {
599+
fileprivate init(_ token: RESPToken) throws(RESPDecodeError) {
634600
guard case .array(let array) = token.value else {
635-
throw .slotsTokenIsNotAnArray
601+
throw RESPDecodeError.tokenMismatch(expected: [.array], token: token)
636602
}
637603

638604
var slotRanges = [ClosedRange<HashSlot>]()
@@ -648,31 +614,25 @@ extension HashSlots {
648614
slotRanges.append(ClosedRange<HashSlot>(uncheckedBounds: (start, end)))
649615
}
650616

617+
if slotRanges.isEmpty { throw RESPDecodeError.invalidArraySize(array, minExpectedSize: 1) }
651618
self = slotRanges
652619
}
653620
}
654621

655622
extension [ValkeyClusterDescription.Node] {
656-
fileprivate init(_ iterator: inout RESPToken.Array.Iterator) throws(ValkeyClusterParseError.Reason) {
657-
guard let token = iterator.next() else {
658-
throw .nodesTokenIsNotAnArray
659-
}
660-
self = try Self(token)
661-
}
662-
663-
fileprivate init(_ token: RESPToken) throws(ValkeyClusterParseError.Reason) {
623+
fileprivate init(_ token: RESPToken) throws(RESPDecodeError) {
664624
guard case .array(let array) = token.value else {
665-
throw .nodesTokenIsNotAnArray
625+
throw RESPDecodeError.tokenMismatch(expected: [.array], token: token)
666626
}
667627

668-
self = try array.map { token throws(ValkeyClusterParseError.Reason) in
628+
self = try array.map { token throws(RESPDecodeError) in
669629
try ValkeyClusterDescription.Node(token)
670630
}
671631
}
672632
}
673633

674634
extension ValkeyClusterDescription.Shard {
675-
fileprivate init(_ token: RESPToken) throws(ValkeyClusterParseError.Reason) {
635+
fileprivate init(_ token: RESPToken) throws(RESPDecodeError) {
676636
switch token.value {
677637
case .array(let array):
678638
self = try Self.makeFromTokenSequence(MapStyleArray(underlying: array))
@@ -688,13 +648,13 @@ extension ValkeyClusterDescription.Shard {
688648
self = try Self.makeFromTokenSequence(mapped)
689649

690650
default:
691-
throw ValkeyClusterParseError.Reason.shardTokenIsNotAnArrayOrMap
651+
throw RESPDecodeError.tokenMismatch(expected: [.array, .map], token: token)
692652
}
693653
}
694654

695655
fileprivate static func makeFromTokenSequence<TokenSequence: Sequence>(
696656
_ sequence: TokenSequence
697-
) throws(ValkeyClusterParseError.Reason) -> Self where TokenSequence.Element == (String, RESPToken) {
657+
) throws(RESPDecodeError) -> Self where TokenSequence.Element == (String, RESPToken) {
698658
var slotRanges = HashSlots()
699659
var nodes: [ValkeyClusterDescription.Node] = []
700660

@@ -711,18 +671,24 @@ extension ValkeyClusterDescription.Shard {
711671
}
712672
}
713673

714-
if nodes.isEmpty { throw .shardIsMissingNode }
715-
if slotRanges.isEmpty { throw .shardIsMissingHashSlots }
716-
717674
return .init(slots: slotRanges, nodes: nodes)
718675
}
719676
}
720677

721678
extension ValkeyClusterDescription.Node {
722-
fileprivate init(_ token: RESPToken) throws(ValkeyClusterParseError.Reason) {
679+
fileprivate init(_ token: RESPToken) throws(RESPDecodeError) {
723680
switch token.value {
724681
case .array(let array):
725-
self = try Self.makeFromTokenSequence(MapStyleArray(underlying: array))
682+
do {
683+
self = try Self.makeFromTokenSequence(MapStyleArray(underlying: array))
684+
} catch {
685+
switch error {
686+
case .decodeError(let error):
687+
throw error
688+
case .missingRequiredValue:
689+
throw RESPDecodeError(.missingToken, token: token, message: "Missing required token for Node")
690+
}
691+
}
726692

727693
case .map(let map):
728694
let mapped = map.lazy.compactMap { (keyNode, value) -> (String, RESPToken)? in
@@ -732,16 +698,30 @@ extension ValkeyClusterDescription.Node {
732698
return nil
733699
}
734700
}
735-
self = try Self.makeFromTokenSequence(mapped)
701+
do {
702+
self = try Self.makeFromTokenSequence(mapped)
703+
} catch {
704+
switch error {
705+
case .decodeError(let error):
706+
throw error
707+
case .missingRequiredValue:
708+
throw RESPDecodeError(.missingToken, token: token, message: "Missing required token for Node")
709+
}
710+
}
736711

737712
default:
738-
throw .nodeTokenIsNotAnArrayOrMap
713+
throw RESPDecodeError.tokenMismatch(expected: [.array, .map], token: token)
739714
}
740715
}
741716

717+
fileprivate enum TokenSequenceError: Error {
718+
case decodeError(RESPDecodeError)
719+
case missingRequiredValue
720+
}
721+
742722
fileprivate static func makeFromTokenSequence<TokenSequence: Sequence>(
743723
_ sequence: TokenSequence
744-
) throws(ValkeyClusterParseError.Reason) -> Self where TokenSequence.Element == (String, RESPToken) {
724+
) throws(TokenSequenceError) -> Self where TokenSequence.Element == (String, RESPToken) {
745725
var id: String?
746726
var port: Int64?
747727
var tlsPort: Int64?
@@ -769,7 +749,7 @@ extension ValkeyClusterDescription.Node {
769749
endpoint = try? String(fromRESP: nodeVal)
770750
case "role":
771751
guard let roleString = try? String(fromRESP: nodeVal), let roleValue = ValkeyClusterDescription.Node.Role(rawValue: roleString) else {
772-
throw .invalidNodeRole
752+
throw .decodeError(RESPDecodeError(.unexpectedToken, token: nodeVal, message: "Invalid Role String"))
773753
}
774754
role = roleValue
775755

@@ -779,7 +759,7 @@ extension ValkeyClusterDescription.Node {
779759
guard let healthString = try? String(fromRESP: nodeVal),
780760
let healthValue = ValkeyClusterDescription.Node.Health(rawValue: healthString)
781761
else {
782-
throw .invalidNodeHealth
762+
throw .decodeError(RESPDecodeError(.unexpectedToken, token: nodeVal, message: "Invalid Node Health String"))
783763
}
784764
health = healthValue
785765

@@ -791,12 +771,12 @@ extension ValkeyClusterDescription.Node {
791771
guard let id = id, let ip = ip, let endpoint = endpoint, let role = role,
792772
let replicationOffset = replicationOffset, let health = health
793773
else {
794-
throw .missingRequiredValueForNode
774+
throw .missingRequiredValue
795775
}
796776

797777
// we need at least port or tlsport
798778
if port == nil && tlsPort == nil {
799-
throw .missingRequiredValueForNode
779+
throw .missingRequiredValue
800780
}
801781

802782
return ValkeyClusterDescription.Node(

Sources/Valkey/RESP/RESPDecodeError.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
//
88
/// Error returned when decoding a RESPToken.
99
/// Error thrown when decoding RESPTokens
10-
public struct RESPDecodeError: Error {
10+
public struct RESPDecodeError: Error, Equatable {
1111
/// Error code for decode error
1212
public struct ErrorCode: Sendable, Equatable, CustomStringConvertible {
1313
fileprivate enum Code: Sendable, Equatable {
@@ -81,6 +81,15 @@ public struct RESPDecodeError: Error {
8181
message: message
8282
)
8383
}
84+
/// Does not match the expected array size
85+
public static func invalidArraySize(_ token: RESPToken, expectedSize: Int? = nil, minExpectedSize: Int? = nil) -> Self {
86+
switch token.value {
87+
case .array(let array):
88+
return invalidArraySize(array, expectedSize: expectedSize, minExpectedSize: minExpectedSize)
89+
default:
90+
return .tokenMismatch(expected: [.array], token: token)
91+
}
92+
}
8493
/// Token associated with key is missing
8594
public static func missingToken(key: String, token: RESPToken) -> Self {
8695
.init(.missingToken, token: token, message: "Expected map to contain token with key \"\(key)\"")

Tests/ValkeyTests/Cluster/ValkeyClusterDescriptionTests.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ struct ValkeyClusterDescriptionTests {
103103
])
104104
let token = RESPToken(val)
105105

106-
#expect(throws: ValkeyClusterParseError(reason: .invalidNodeHealth, token: token)) {
106+
#expect(throws: RESPDecodeError(.unexpectedToken, token: .init(.bulkString("invalid-health-state")), message: "Invalid Node Health String")) {
107107
_ = try ValkeyClusterDescription(fromRESP: token)
108108
}
109109
}
@@ -112,7 +112,7 @@ struct ValkeyClusterDescriptionTests {
112112
func testSlotsAreNotAnArray() throws {
113113
// Non-array token for cluster description
114114
let singleValueToken = RESPToken(RESP3Value.bulkString("not-an-array"))
115-
#expect(throws: ValkeyClusterParseError.self) {
115+
#expect(throws: RESPDecodeError.tokenMismatch(expected: [.array], token: .init(.bulkString("not-an-array")))) {
116116
_ = try ValkeyClusterDescription(fromRESP: singleValueToken)
117117
}
118118

@@ -145,7 +145,7 @@ struct ValkeyClusterDescriptionTests {
145145
])
146146
)
147147

148-
#expect(throws: ValkeyClusterParseError(reason: .slotsTokenIsNotAnArray, token: invalidSlotsToken)) {
148+
#expect(throws: RESPDecodeError.tokenMismatch(expected: [.array], token: .init(.bulkString("not-an-array")))) {
149149
try ValkeyClusterDescription(fromRESP: invalidSlotsToken)
150150
}
151151

@@ -161,7 +161,7 @@ struct ValkeyClusterDescriptionTests {
161161
])
162162
)
163163

164-
#expect(throws: ValkeyClusterParseError(reason: .nodesTokenIsNotAnArray, token: invalidNodesToken)) {
164+
#expect(throws: RESPDecodeError.tokenMismatch(expected: [.array], token: .init(.bulkString("not-an-array")))) {
165165
_ = try ValkeyClusterDescription(fromRESP: invalidNodesToken)
166166
}
167167
}
@@ -197,7 +197,7 @@ struct ValkeyClusterDescriptionTests {
197197
let token = RESPToken(valWithMultipleErrors)
198198

199199
// The error we expect to see first is the invalid role
200-
#expect(throws: ValkeyClusterParseError(reason: .invalidNodeRole, token: token)) {
200+
#expect(throws: RESPDecodeError(.unexpectedToken, token: .init(.bulkString("invalid-role")), message: "Invalid Role String")) {
201201
_ = try ValkeyClusterDescription(fromRESP: token)
202202
}
203203
}

0 commit comments

Comments
 (0)