Sfoglia il codice sorgente

Allow metadata to be mutated on server response types (#2120)

Motivation:

The server response types have a metadata computed property with only a
getter. It's entirely possible to mutate the metadata manually, it's
just a bit of a faff. This should be easier.

Modifications:

- Add a setter to the computed property
- Migrate server response tests to swift-testing

Result:

- Easier to use API
George Barnett 1 anno fa
parent
commit
dd22b3938e

+ 36 - 16
Sources/GRPCCore/Call/Server/ServerResponse.swift

@@ -244,15 +244,25 @@ extension ServerResponse {
     self.accepted = .failure(error)
   }
 
-  /// Returns the metadata to be sent to the client at the start of the response.
-  ///
-  /// For rejected RPCs (in other words, where ``accepted`` is `failure`) the metadata is empty.
+  /// The metadata to be sent to the client at the start of the response.
   public var metadata: Metadata {
-    switch self.accepted {
-    case let .success(contents):
-      return contents.metadata
-    case .failure:
-      return [:]
+    get {
+      switch self.accepted {
+      case let .success(contents):
+        return contents.metadata
+      case .failure(let error):
+        return error.metadata
+      }
+    }
+    set {
+      switch self.accepted {
+      case var .success(contents):
+        contents.metadata = newValue
+        self.accepted = .success(contents)
+      case var .failure(error):
+        error.metadata = newValue
+        self.accepted = .failure(error)
+      }
     }
   }
 
@@ -303,15 +313,25 @@ extension StreamingServerResponse {
     self.accepted = .failure(error)
   }
 
-  /// Returns metadata received from the server at the start of the response.
-  ///
-  /// For rejected RPCs (in other words, where ``accepted`` is `failure`) the metadata is empty.
+  /// The metadata to be sent to the client at the start of the response.
   public var metadata: Metadata {
-    switch self.accepted {
-    case let .success(contents):
-      return contents.metadata
-    case .failure:
-      return [:]
+    get {
+      switch self.accepted {
+      case let .success(contents):
+        return contents.metadata
+      case .failure(let error):
+        return error.metadata
+      }
+    }
+    set {
+      switch self.accepted {
+      case var .success(contents):
+        contents.metadata = newValue
+        self.accepted = .success(contents)
+      case var .failure(error):
+        error.metadata = newValue
+        self.accepted = .failure(error)
+      }
     }
   }
 }

+ 71 - 38
Tests/GRPCCoreTests/Call/Server/ServerResponseTests.swift

@@ -13,38 +13,41 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-@_spi(Testing) import GRPCCore
-import XCTest
 
-final class ServerResponseTests: XCTestCase {
-  func testSingleConvenienceInit() {
-    var response = ServerResponse(
+import GRPCCore
+import Testing
+
+@Suite("ServerResponse")
+struct ServerResponseTests {
+  @Test("ServerResponse(message:metadata:trailingMetadata:)")
+  func responseInitSuccess() throws {
+    let response = ServerResponse(
       message: "message",
       metadata: ["metadata": "initial"],
       trailingMetadata: ["metadata": "trailing"]
     )
 
-    switch response.accepted {
-    case .success(let contents):
-      XCTAssertEqual(contents.message, "message")
-      XCTAssertEqual(contents.metadata, ["metadata": "initial"])
-      XCTAssertEqual(contents.trailingMetadata, ["metadata": "trailing"])
-    case .failure:
-      XCTFail("Unexpected error")
-    }
+    let contents = try #require(try response.accepted.get())
+    #expect(contents.message == "message")
+    #expect(contents.metadata == ["metadata": "initial"])
+    #expect(contents.trailingMetadata == ["metadata": "trailing"])
+  }
 
+  @Test("ServerResponse(of:error:)")
+  func responseInitError() throws {
     let error = RPCError(code: .aborted, message: "Aborted")
-    response = ServerResponse(of: String.self, error: error)
+    let response = ServerResponse(of: String.self, error: error)
     switch response.accepted {
     case .success:
-      XCTFail("Unexpected success")
-    case .failure(let error):
-      XCTAssertEqual(error, error)
+      Issue.record("Expected error")
+    case .failure(let rpcError):
+      #expect(rpcError == error)
     }
   }
 
-  func testStreamConvenienceInit() async throws {
-    var response = StreamingServerResponse(
+  @Test("StreamingServerResponse(of:metadata:producer:)")
+  func streamingResponseInitSuccess() async throws {
+    let response = StreamingServerResponse(
       of: String.self,
       metadata: ["metadata": "initial"]
     ) { _ in
@@ -52,26 +55,26 @@ final class ServerResponseTests: XCTestCase {
       return ["metadata": "trailing"]
     }
 
-    switch response.accepted {
-    case .success(let contents):
-      XCTAssertEqual(contents.metadata, ["metadata": "initial"])
-      let trailingMetadata = try await contents.producer(.failTestOnWrite())
-      XCTAssertEqual(trailingMetadata, ["metadata": "trailing"])
-    case .failure:
-      XCTFail("Unexpected error")
-    }
+    let contents = try #require(try response.accepted.get())
+    #expect(contents.metadata == ["metadata": "initial"])
+    let trailingMetadata = try await contents.producer(.failTestOnWrite())
+    #expect(trailingMetadata == ["metadata": "trailing"])
+  }
 
+  @Test("StreamingServerResponse(of:error:)")
+  func streamingResponseInitError() async throws {
     let error = RPCError(code: .aborted, message: "Aborted")
-    response = StreamingServerResponse(of: String.self, error: error)
+    let response = StreamingServerResponse(of: String.self, error: error)
     switch response.accepted {
     case .success:
-      XCTFail("Unexpected success")
-    case .failure(let error):
-      XCTAssertEqual(error, error)
+      Issue.record("Expected error")
+    case .failure(let rpcError):
+      #expect(rpcError == error)
     }
   }
 
-  func testSingleToStreamConversionForSuccessfulResponse() async throws {
+  @Test("StreamingServerResponse(single:) (accepted)")
+  func singleToStreamConversionForSuccessfulResponse() async throws {
     let single = ServerResponse(
       message: "foo",
       metadata: ["metadata": "initial"],
@@ -90,19 +93,49 @@ final class ServerResponseTests: XCTestCase {
       throw error
     }
 
-    XCTAssertEqual(stream.metadata, ["metadata": "initial"])
+    #expect(stream.metadata == ["metadata": "initial"])
     let collected = try await messages.collect()
-    XCTAssertEqual(collected, ["foo"])
-    XCTAssertEqual(trailingMetadata, ["metadata": "trailing"])
+    #expect(collected == ["foo"])
+    #expect(trailingMetadata == ["metadata": "trailing"])
   }
 
-  func testSingleToStreamConversionForFailedResponse() async throws {
+  @Test("StreamingServerResponse(single:) (rejected)")
+  func singleToStreamConversionForFailedResponse() async throws {
     let error = RPCError(code: .aborted, message: "aborted")
     let single = ServerResponse(of: String.self, error: error)
     let stream = StreamingServerResponse(single: single)
 
-    XCTAssertThrowsRPCError(try stream.accepted.get()) {
-      XCTAssertEqual($0, error)
+    switch stream.accepted {
+    case .success:
+      Issue.record("Expected error")
+    case .failure(let rpcError):
+      #expect(rpcError == error)
+    }
+  }
+
+  @Test("Mutate metadata on response", arguments: [true, false])
+  func mutateMetadataOnResponse(accepted: Bool) {
+    var response: ServerResponse<String>
+    if accepted {
+      response = ServerResponse(message: "")
+    } else {
+      response = ServerResponse(error: RPCError(code: .aborted, message: ""))
     }
+
+    response.metadata.addString("value", forKey: "key")
+    #expect(response.metadata == ["key": "value"])
+  }
+
+  @Test("Mutate metadata on streaming response", arguments: [true, false])
+  func mutateMetadataOnStreamingResponse(accepted: Bool) {
+    var response: StreamingServerResponse<String>
+    if accepted {
+      response = StreamingServerResponse { _ in [:] }
+    } else {
+      response = StreamingServerResponse(error: RPCError(code: .aborted, message: ""))
+    }
+
+    response.metadata.addString("value", forKey: "key")
+    #expect(response.metadata == ["key": "value"])
   }
 }