Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow metadata to be mutated on server response types #2120

Merged
merged 1 commit into from
Nov 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Comment on lines +258 to +264
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a side note: This might be prone to CoWs and something we might want to make @inlinable or use a consuming switch

}
}
}

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"])
}
}
Loading