Skip to content

Commit 7d723f5

Browse files
committed
Add option to flatten RPCError causes
1 parent 74d0848 commit 7d723f5

File tree

2 files changed

+139
-74
lines changed

2 files changed

+139
-74
lines changed

Sources/GRPCCore/RPCError.swift

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,46 @@ public struct RPCError: Sendable, Hashable, Error {
4242
/// - message: A message providing additional context about the code.
4343
/// - metadata: Any metadata to attach to the error.
4444
/// - cause: An underlying error which led to this error being thrown.
45-
public init(code: Code, message: String, metadata: Metadata = [:], cause: (any Error)? = nil) {
46-
self.code = code
47-
self.message = message
48-
self.metadata = metadata
49-
self.cause = cause
45+
/// - flatteningCauses: Whether to flatten the `causes` as long as they're ``RPCError``s
46+
/// sharing the same ``Code-swift.struct``.
47+
public init(
48+
code: Code,
49+
message: String,
50+
metadata: Metadata = [:],
51+
cause: (any Error)? = nil,
52+
flatteningCauses: Bool = false
53+
) {
54+
if flatteningCauses {
55+
var finalMessage = message
56+
var finalMetadata = metadata
57+
var nextCause = cause
58+
while let nextRPCErrorCause = nextCause as? RPCError {
59+
if code == nextRPCErrorCause.code {
60+
finalMessage = finalMessage + " \(nextRPCErrorCause.message)"
61+
finalMetadata.merge(nextRPCErrorCause.metadata)
62+
nextCause = nextRPCErrorCause.cause
63+
} else {
64+
nextCause = RPCError(
65+
code: nextRPCErrorCause.code,
66+
message: nextRPCErrorCause.message,
67+
metadata: nextRPCErrorCause.metadata,
68+
cause: nextRPCErrorCause.cause,
69+
flatteningCauses: true
70+
)
71+
break
72+
}
73+
}
74+
75+
self.code = code
76+
self.message = finalMessage
77+
self.metadata = finalMetadata
78+
self.cause = nextCause
79+
} else {
80+
self.code = code
81+
self.message = message
82+
self.metadata = metadata
83+
self.cause = cause
84+
}
5085
}
5186

5287
/// Create a new RPC error from the provided ``Status``.

Tests/GRPCCoreTests/RPCErrorTests.swift

Lines changed: 99 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -14,114 +14,144 @@
1414
* limitations under the License.
1515
*/
1616
import GRPCCore
17-
import XCTest
18-
19-
final class RPCErrorTests: XCTestCase {
20-
private static let statusCodeRawValue: [(RPCError.Code, Int)] = [
21-
(.cancelled, 1),
22-
(.unknown, 2),
23-
(.invalidArgument, 3),
24-
(.deadlineExceeded, 4),
25-
(.notFound, 5),
26-
(.alreadyExists, 6),
27-
(.permissionDenied, 7),
28-
(.resourceExhausted, 8),
29-
(.failedPrecondition, 9),
30-
(.aborted, 10),
31-
(.outOfRange, 11),
32-
(.unimplemented, 12),
33-
(.internalError, 13),
34-
(.unavailable, 14),
35-
(.dataLoss, 15),
36-
(.unauthenticated, 16),
37-
]
17+
import Testing
3818

19+
@Suite("RPCError Tests")
20+
struct RPCErrorTests {
21+
@Test("Custom String Convertible")
3922
func testCustomStringConvertible() {
40-
XCTAssertDescription(RPCError(code: .dataLoss, message: ""), #"dataLoss: """#)
41-
XCTAssertDescription(RPCError(code: .unknown, message: "message"), #"unknown: "message""#)
42-
XCTAssertDescription(RPCError(code: .aborted, message: "message"), #"aborted: "message""#)
23+
#expect(String(describing: RPCError(code: .dataLoss, message: "")) == #"dataLoss: """#)
24+
#expect(String(describing: RPCError(code: .unknown, message: "message")) == #"unknown: "message""#)
25+
#expect(String(describing: RPCError(code: .aborted, message: "message")) == #"aborted: "message""#)
4326

4427
struct TestError: Error {}
45-
XCTAssertDescription(
46-
RPCError(code: .aborted, message: "message", cause: TestError()),
28+
#expect(
29+
String(describing: RPCError(code: .aborted, message: "message", cause: TestError()))
30+
==
4731
#"aborted: "message" (cause: "TestError()")"#
4832
)
4933
}
5034

35+
@Test("Error from Status")
5136
func testErrorFromStatus() throws {
5237
var status = Status(code: .ok, message: "")
5338
// ok isn't an error
54-
XCTAssertNil(RPCError(status: status))
39+
#expect(RPCError(status: status) == nil)
5540

5641
status.code = .invalidArgument
57-
var error = try XCTUnwrap(RPCError(status: status))
58-
XCTAssertEqual(error.code, .invalidArgument)
59-
XCTAssertEqual(error.message, "")
60-
XCTAssertEqual(error.metadata, [:])
42+
var error = RPCError(status: status)
43+
try #require(error != nil)
44+
#expect(error!.code == .invalidArgument)
45+
#expect(error!.message == "")
46+
#expect(error!.metadata == [:])
6147

6248
status.code = .cancelled
6349
status.message = "an error message"
64-
error = try XCTUnwrap(RPCError(status: status))
65-
XCTAssertEqual(error.code, .cancelled)
66-
XCTAssertEqual(error.message, "an error message")
67-
XCTAssertEqual(error.metadata, [:])
50+
error = RPCError(status: status)
51+
try #require(error != nil)
52+
#expect(error!.code == .cancelled)
53+
#expect(error!.message == "an error message")
54+
#expect(error!.metadata == [:])
6855
}
6956

70-
func testErrorCodeFromStatusCode() throws {
71-
XCTAssertNil(RPCError.Code(Status.Code.ok))
72-
XCTAssertEqual(RPCError.Code(Status.Code.cancelled), .cancelled)
73-
XCTAssertEqual(RPCError.Code(Status.Code.unknown), .unknown)
74-
XCTAssertEqual(RPCError.Code(Status.Code.invalidArgument), .invalidArgument)
75-
XCTAssertEqual(RPCError.Code(Status.Code.deadlineExceeded), .deadlineExceeded)
76-
XCTAssertEqual(RPCError.Code(Status.Code.notFound), .notFound)
77-
XCTAssertEqual(RPCError.Code(Status.Code.alreadyExists), .alreadyExists)
78-
XCTAssertEqual(RPCError.Code(Status.Code.permissionDenied), .permissionDenied)
79-
XCTAssertEqual(RPCError.Code(Status.Code.resourceExhausted), .resourceExhausted)
80-
XCTAssertEqual(RPCError.Code(Status.Code.failedPrecondition), .failedPrecondition)
81-
XCTAssertEqual(RPCError.Code(Status.Code.aborted), .aborted)
82-
XCTAssertEqual(RPCError.Code(Status.Code.outOfRange), .outOfRange)
83-
XCTAssertEqual(RPCError.Code(Status.Code.unimplemented), .unimplemented)
84-
XCTAssertEqual(RPCError.Code(Status.Code.internalError), .internalError)
85-
XCTAssertEqual(RPCError.Code(Status.Code.unavailable), .unavailable)
86-
XCTAssertEqual(RPCError.Code(Status.Code.dataLoss), .dataLoss)
87-
XCTAssertEqual(RPCError.Code(Status.Code.unauthenticated), .unauthenticated)
57+
@Test(
58+
"Error Code from Status Code",
59+
arguments: [
60+
(Status.Code.ok, nil),
61+
(Status.Code.cancelled, RPCError.Code.cancelled),
62+
(Status.Code.unknown, RPCError.Code.unknown),
63+
(Status.Code.invalidArgument, RPCError.Code.invalidArgument),
64+
(Status.Code.deadlineExceeded, RPCError.Code.deadlineExceeded),
65+
(Status.Code.notFound, RPCError.Code.notFound),
66+
(Status.Code.alreadyExists, RPCError.Code.alreadyExists),
67+
(Status.Code.permissionDenied, RPCError.Code.permissionDenied),
68+
(Status.Code.resourceExhausted, RPCError.Code.resourceExhausted),
69+
(Status.Code.failedPrecondition, RPCError.Code.failedPrecondition),
70+
(Status.Code.aborted, RPCError.Code.aborted),
71+
(Status.Code.outOfRange, RPCError.Code.outOfRange),
72+
(Status.Code.unimplemented, RPCError.Code.unimplemented),
73+
(Status.Code.internalError, RPCError.Code.internalError),
74+
(Status.Code.unavailable, RPCError.Code.unavailable),
75+
(Status.Code.dataLoss, RPCError.Code.dataLoss),
76+
(Status.Code.unauthenticated, RPCError.Code.unauthenticated)
77+
]
78+
)
79+
func testErrorCodeFromStatusCode(statusCode: Status.Code, rpcErrorCode: RPCError.Code?) throws {
80+
#expect(RPCError.Code(statusCode) == rpcErrorCode)
8881
}
8982

83+
@Test("Equatable Conformance")
9084
func testEquatableConformance() {
91-
XCTAssertEqual(
92-
RPCError(code: .cancelled, message: ""),
85+
#expect(
86+
RPCError(code: .cancelled, message: "")
87+
==
9388
RPCError(code: .cancelled, message: "")
9489
)
9590

96-
XCTAssertEqual(
97-
RPCError(code: .cancelled, message: "message"),
91+
#expect(
92+
RPCError(code: .cancelled, message: "message")
93+
==
9894
RPCError(code: .cancelled, message: "message")
9995
)
10096

101-
XCTAssertEqual(
102-
RPCError(code: .cancelled, message: "message", metadata: ["foo": "bar"]),
97+
#expect(
98+
RPCError(code: .cancelled, message: "message", metadata: ["foo": "bar"])
99+
==
103100
RPCError(code: .cancelled, message: "message", metadata: ["foo": "bar"])
104101
)
105102

106-
XCTAssertNotEqual(
107-
RPCError(code: .cancelled, message: ""),
103+
#expect(
104+
RPCError(code: .cancelled, message: "")
105+
!=
108106
RPCError(code: .cancelled, message: "message")
109107
)
110108

111-
XCTAssertNotEqual(
112-
RPCError(code: .cancelled, message: "message"),
109+
#expect(
110+
RPCError(code: .cancelled, message: "message")
111+
!=
113112
RPCError(code: .unknown, message: "message")
114113
)
115114

116-
XCTAssertNotEqual(
117-
RPCError(code: .cancelled, message: "message", metadata: ["foo": "bar"]),
115+
#expect(
116+
RPCError(code: .cancelled, message: "message", metadata: ["foo": "bar"])
117+
!=
118118
RPCError(code: .cancelled, message: "message", metadata: ["foo": "baz"])
119119
)
120120
}
121121

122-
func testStatusCodeRawValues() {
123-
for (code, expected) in Self.statusCodeRawValue {
124-
XCTAssertEqual(code.rawValue, expected, "\(code) had unexpected raw value")
125-
}
122+
@Test("Status Code Raw Values", arguments: [
123+
(RPCError.Code.cancelled, 1),
124+
(.unknown, 2),
125+
(.invalidArgument, 3),
126+
(.deadlineExceeded, 4),
127+
(.notFound, 5),
128+
(.alreadyExists, 6),
129+
(.permissionDenied, 7),
130+
(.resourceExhausted, 8),
131+
(.failedPrecondition, 9),
132+
(.aborted, 10),
133+
(.outOfRange, 11),
134+
(.unimplemented, 12),
135+
(.internalError, 13),
136+
(.unavailable, 14),
137+
(.dataLoss, 15),
138+
(.unauthenticated, 16),
139+
])
140+
func testStatusCodeRawValues(statusCode: RPCError.Code, rawValue: Int) {
141+
#expect(statusCode.rawValue == rawValue, "\(statusCode) had unexpected raw value")
142+
}
143+
144+
@Test("Flatten causes with same status code")
145+
func testFlattenCausesWithSameStatusCode() {
146+
let error1 = RPCError(code: .unknown, message: "Error 1.")
147+
let error2 = RPCError(code: .unknown, message: "Error 2.", cause: error1)
148+
let error3 = RPCError(code: .dataLoss, message: "Error 3.", cause: error2)
149+
let error4 = RPCError(code: .aborted, message: "Error 4.", cause: error3)
150+
let error5 = RPCError(code: .aborted, message: "Error 5.", cause: error4, flatteningCauses: true)
151+
152+
let unknownMerged = RPCError(code: .unknown, message: "Error 2. Error 1.")
153+
let dataLossMerged = RPCError(code: .dataLoss, message: "Error 3.", cause: unknownMerged)
154+
let abortedMerged = RPCError(code: .aborted, message: "Error 5. Error 4.", cause: dataLossMerged)
155+
#expect(error5 == abortedMerged)
126156
}
127157
}

0 commit comments

Comments
 (0)