Skip to content

Commit

Permalink
Allow metadata to be mutated on server response types (#2120)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
glbrntt authored Nov 15, 2024
1 parent e160fd0 commit dd22b39
Show file tree
Hide file tree
Showing 2 changed files with 107 additions and 54 deletions.
52 changes: 36 additions & 16 deletions Sources/GRPCCore/Call/Server/ServerResponse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}

Expand Down Expand Up @@ -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)
}
}
}
}
Expand Down
109 changes: 71 additions & 38 deletions Tests/GRPCCoreTests/Call/Server/ServerResponseTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,65 +13,68 @@
* 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
// Empty body.
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"],
Expand All @@ -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"])
}
}

0 comments on commit dd22b39

Please sign in to comment.