Skip to content

Commit 06471a2

Browse files
committed
Change RedisClientError from enum to struct
Motivation: The current state of Swift does not leave room for library evolution of enum types used for `Error`. To avoid having to increment Major SemVer to add a new error case that might be needed to fix a bug, the `enum-like struct` idiom should be used. Ideally this idiom will disappear, when Swift provides a way for Swift Packages to have a "library evolution" capability. See https://forums.swift.org/t/extensible-enumerations-for-non-resilient-libraries/35900 Modifications: - Change: `RedisClientError` to be struct with private enum value Result: Should new error cases be necessary to add, they can be in Minor SemVer releases, rather than Major SemVer.
1 parent 60e79b7 commit 06471a2

File tree

2 files changed

+74
-26
lines changed

2 files changed

+74
-26
lines changed

Sources/RediStack/RedisClient.swift

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414

15+
import protocol Foundation.LocalizedError
1516
import struct Logging.Logger
1617
import NIO
1718

@@ -60,3 +61,61 @@ extension RedisClient {
6061
return self
6162
}
6263
}
64+
65+
/// When working with `RedisClient`, runtime errors can be thrown to indicate problems with connection state, decoding assertions, or otherwise.
66+
public struct RedisClientError: LocalizedError, Equatable, Hashable {
67+
/// The connection is closed, but was used to try and send a command to Redis.
68+
public static let connectionClosed = RedisClientError(.connectionClosed)
69+
70+
/// Conversion from `RESPValue` to the specified type failed.
71+
///
72+
/// If this is ever triggered, please capture the original `RESPValue` string sent from Redis for bug reports.
73+
public static func failedRESPConversion(to type: Any.Type) -> RedisClientError {
74+
return .init(.failedRESPConversion(to: type))
75+
}
76+
77+
/// Expectations of message structures were not met.
78+
///
79+
/// If this is ever triggered, please capture the original `RESPValue` string sent from Redis along with the command and arguments sent to Redis for bug reports.
80+
public static func assertionFailure(message: String) -> RedisClientError {
81+
return .init(.assertionFailure(message: message))
82+
}
83+
84+
public var errorDescription: String? {
85+
let message: String
86+
switch self.baseError {
87+
case .connectionClosed: message = "Connection was closed while trying to send command."
88+
case let .failedRESPConversion(type): message = "Failed to convert RESP to \(type)"
89+
case let .assertionFailure(text): message = text
90+
}
91+
return "(RediStack) \(message)"
92+
}
93+
94+
public var recoverySuggestion: String? {
95+
switch self.baseError {
96+
case .connectionClosed: return "Check that the connection is not closed before invoking commands. With RedisConnection, this can be done with the 'isConnected' property."
97+
case .failedRESPConversion: return "Ensure that the data type being requested is actually what's being returned. If you see this error and are not sure why, capture the original RESPValue string sent from Redis to add to your bug report."
98+
case .assertionFailure: return "This error should in theory never happen. If you trigger this error, capture the original RESPValue string sent from Redis along with the command and arguments that you sent to Redis to add to your bug report."
99+
}
100+
}
101+
102+
private var baseError: BaseError
103+
104+
private init(_ baseError: BaseError) { self.baseError = baseError }
105+
106+
/* Protocol Conformances and Private Type implementation */
107+
108+
public static func ==(lhs: RedisClientError, rhs: RedisClientError) -> Bool {
109+
return lhs.localizedDescription == rhs.localizedDescription
110+
}
111+
112+
public func hash(into hasher: inout Hasher) {
113+
hasher.combine(self.localizedDescription)
114+
}
115+
116+
fileprivate enum BaseError {
117+
case connectionClosed
118+
case failedRESPConversion(to: Any.Type)
119+
case assertionFailure(message: String)
120+
}
121+
}

Sources/RediStack/RedisErrors.swift renamed to Sources/RediStack/RedisError.swift

Lines changed: 15 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -14,41 +14,30 @@
1414

1515
import protocol Foundation.LocalizedError
1616

17-
/// When working with `RedisClient`, runtime errors can be thrown to indicate problems with connection state, decoding assertions, or otherwise.
18-
public enum RedisClientError: LocalizedError {
19-
/// The connection is closed, but was used to try and send a command to Redis.
20-
case connectionClosed
21-
/// Conversion from `RESPValue` to the specified type failed.
22-
/// If this is ever triggered, please capture the original `RESPValue` value.
23-
case failedRESPConversion(to: Any.Type)
24-
/// Expectations of message structures were not met.
25-
/// If this is ever triggered, please capture the original byte message from Redis along with the command and arguments to Redis.
26-
case assertionFailure(message: String)
27-
28-
public var errorDescription: String? {
29-
let message: String
30-
switch self {
31-
case .connectionClosed: message = "Connection was closed while trying to send command."
32-
case let .failedRESPConversion(type): message = "Failed to convert RESP to \(type)"
33-
case let .assertionFailure(text): message = text
34-
}
35-
return "RediStack: \(message)"
36-
}
37-
}
38-
3917
/// If something goes wrong with a command within Redis, it will respond with an error that is captured and represented by instances of this type.
40-
public struct RedisError: LocalizedError, Equatable {
18+
public struct RedisError: LocalizedError {
19+
/// The error message from Redis, prefixed with `(Redis)` to indicate the message was from Redis itself.
4120
public let message: String
4221

4322
public var errorDescription: String? { return message }
4423

24+
/// Creates a new instance of an error from a Redis instance.
25+
/// - Parameter reason: The error reason from Redis.
4526
public init(reason: String) {
46-
message = "Redis: \(reason)"
27+
message = "(Redis) \(reason)"
4728
}
48-
49-
public static func == (lhs: RedisError, rhs: RedisError) -> Bool {
29+
}
30+
31+
// MARK: Equatable, Hashable
32+
33+
extension RedisError: Equatable, Hashable {
34+
public static func ==(lhs: RedisError, rhs: RedisError) -> Bool {
5035
return lhs.message == rhs.message
5136
}
37+
38+
public func hash(into hasher: inout Hasher) {
39+
hasher.combine(self.message)
40+
}
5241
}
5342

5443
// MARK: RESPValueConvertible

0 commit comments

Comments
 (0)