Browse Source

Allow single response to contain initial metadata and error (#1927)

Motivation:

Currently the client API doesn't allow for users to distinguish between
receiving metadata and a failed status and just a failed status. The
distinction being that the server "accepted" the former request but it
resulted in failure and the rejected the latter.

Modifications:

- Allow this to be represented by turning the message in the contents to
  be a result

Result:

More flexible API
George Barnett 1 year ago
parent
commit
6dad5bc6b6

+ 31 - 5
Sources/GRPCCore/Call/Client/ClientResponse.swift

@@ -59,7 +59,7 @@ extension ClientResponse {
   /// // The explicit API:
   /// switch response {
   /// case .success(let contents):
-  ///   print("Received response with message '\(contents.message)'")
+  ///   print("Received response with message '\(try contents.message.get())'")
   /// case .failure(let error):
   ///   print("RPC failed with code '\(error.code)'")
   /// }
@@ -80,8 +80,9 @@ extension ClientResponse {
       /// level metadata provided by the service.
       public var metadata: Metadata
 
-      /// The response message received from the server.
-      public var message: Message
+      /// The response message received from the server, or an error of the RPC failed with a
+      /// non-ok status.
+      public var message: Result<Message, RPCError>
 
       /// Metadata received from the server at the end of the response.
       ///
@@ -101,9 +102,23 @@ extension ClientResponse {
         trailingMetadata: Metadata
       ) {
         self.metadata = metadata
-        self.message = message
+        self.message = .success(message)
         self.trailingMetadata = trailingMetadata
       }
+
+      /// Creates a `Contents`.
+      ///
+      /// - Parameters:
+      ///   - metadata: Metadata received from the server at the beginning of the response.
+      ///   - error: Error received from the server.
+      public init(
+        metadata: Metadata,
+        error: RPCError
+      ) {
+        self.metadata = metadata
+        self.message = .failure(error)
+        self.trailingMetadata = error.metadata
+      }
     }
 
     /// Whether the RPC was accepted or rejected.
@@ -263,6 +278,17 @@ extension ClientResponse.Single {
     self.accepted = .success(contents)
   }
 
+  /// Creates a new accepted response with a failed outcome.
+  ///
+  /// - Parameters:
+  ///   - messageType: The type of message.
+  ///   - metadata: Metadata received from the server at the beginning of the response.
+  ///   - error: An error describing why the RPC failed.
+  public init(of messageType: Message.Type = Message.self, metadata: Metadata, error: RPCError) {
+    let contents = Contents(metadata: metadata, error: error)
+    self.accepted = .success(contents)
+  }
+
   /// Creates a new failed response.
   ///
   /// - Parameters:
@@ -289,7 +315,7 @@ extension ClientResponse.Single {
   /// - Throws: ``RPCError`` if the request failed.
   public var message: Message {
     get throws {
-      try self.accepted.map { $0.message }.get()
+      try self.accepted.flatMap { $0.message }.get()
     }
   }
 

+ 1 - 1
Sources/GRPCCore/Call/Client/Internal/ClientResponse+Convenience.swift

@@ -70,7 +70,7 @@ extension ClientResponse.Single {
         }
       } catch let error as RPCError {
         // Known error type.
-        self.accepted = .failure(error)
+        self.accepted = .success(Contents(metadata: contents.metadata, error: error))
       } catch {
         // Unexpected, but should be handled nonetheless.
         self.accepted = .failure(RPCError(code: .unknown, message: String(describing: error)))

+ 11 - 0
Tests/GRPCCoreTests/Call/Client/ClientResponseTests.swift

@@ -43,6 +43,17 @@ final class ClientResponseTests: XCTestCase {
     XCTAssertEqual(response.trailingMetadata, ["bar": "baz"])
   }
 
+  func testAcceptedButFailedSingleResponseConvenienceMethods() {
+    let error = RPCError(code: .aborted, message: "error message", metadata: ["bar": "baz"])
+    let response = ClientResponse.Single(of: String.self, metadata: ["foo": "bar"], error: error)
+
+    XCTAssertEqual(response.metadata, ["foo": "bar"])
+    XCTAssertThrowsRPCError(try response.message) {
+      XCTAssertEqual($0, error)
+    }
+    XCTAssertEqual(response.trailingMetadata, ["bar": "baz"])
+  }
+
   func testAcceptedStreamResponseConvenienceMethods() async throws {
     let response = ClientResponse.Stream(
       of: String.self,